From fe5848b25ab840a5764482deb1179ecf25b128ab Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 10 Mar 2025 13:41:35 +0100 Subject: [PATCH 001/374] move rlc to rlccalledmethods, reintroduce rlc as empty checker calling rlccalledmethods --- .../MustCallAnnotatedTypeFactory.java | 9 + .../MustCallConsistencyAnalyzer.java | 162 ++++---- .../resourceleak/MustCallInference.java | 21 +- .../resourceleak/ResourceLeakChecker.java | 52 +-- .../resourceleak/ResourceLeakUtils.java | 362 ++++++++++++++++++ .../IOUtils.astub | 0 .../RLCCalledMethodsAnalysis.java} | 11 +- ...RLCCalledMethodsAnnotatedTypeFactory.java} | 52 +-- .../RLCCalledMethodsChecker.java | 67 ++++ .../RLCCalledMethodsTransfer.java} | 12 +- .../RLCCalledMethodsVisitor.java} | 30 +- .../messages.properties | 0 12 files changed, 610 insertions(+), 168 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java rename checker/src/main/java/org/checkerframework/checker/{resourceleak => rlccalledmethods}/IOUtils.astub (100%) rename checker/src/main/java/org/checkerframework/checker/{resourceleak/ResourceLeakAnalysis.java => rlccalledmethods/RLCCalledMethodsAnalysis.java} (69%) rename checker/src/main/java/org/checkerframework/checker/{resourceleak/ResourceLeakAnnotatedTypeFactory.java => rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java} (92%) create mode 100644 checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java rename checker/src/main/java/org/checkerframework/checker/{resourceleak/ResourceLeakTransfer.java => rlccalledmethods/RLCCalledMethodsTransfer.java} (95%) rename checker/src/main/java/org/checkerframework/checker/{resourceleak/ResourceLeakVisitor.java => rlccalledmethods/RLCCalledMethodsVisitor.java} (96%) rename checker/src/main/java/org/checkerframework/checker/{resourceleak => rlccalledmethods}/messages.properties (100%) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index b137c9de2c6b..a2ab9bcbddf4 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -346,6 +346,15 @@ public ExecutableElement getMustCallValueElement() { return mustCallValueElement; } + /** + * Returns the {@link InheritableMustCall#value} element. + * + * @return the {@link InheritableMustCall#value} element + */ + public ExecutableElement getInheritableMustCallValueElement() { + return inheritableMustCallValueElement; + } + /** Support @InheritableMustCall meaning @MustCall on all subtype elements. */ private class MustCallDefaultQualifierForUseTypeAnnotator extends DefaultQualifierForUseTypeAnnotator { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 6d2552379d17..83c3b945cbd7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -45,6 +45,9 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnalysis; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsVisitor; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -139,8 +142,7 @@ * expressions, method calls (for the return value), and ternary expressions. Other types of * expressions may be supported in the future. */ -/*package-private*/ -class MustCallConsistencyAnalyzer { +public class MustCallConsistencyAnalyzer { /** True if errors related to static owning fields should be suppressed. */ private final boolean permitStaticOwning; @@ -158,11 +160,11 @@ class MustCallConsistencyAnalyzer { * The type factory for the Resource Leak Checker, which is used to get called methods types and * to access the Must Call Checker. */ - private final ResourceLeakAnnotatedTypeFactory typeFactory; + private final RLCCalledMethodsAnnotatedTypeFactory cmAtf; /** - * A cache for the result of calling {@code ResourceLeakAnnotatedTypeFactory.getStoreAfter()} on a - * node. The cache prevents repeatedly computing least upper bounds on stores + * A cache for the result of calling {@code RLCCalledMethodsAnnotatedTypeFactory.getStoreAfter()} + * on a node. The cache prevents repeatedly computing least upper bounds on stores */ private final IdentityHashMap cmStoreAfter = new IdentityHashMap<>(); @@ -176,7 +178,7 @@ class MustCallConsistencyAnalyzer { private final ResourceLeakChecker checker; /** The analysis from the Resource Leak Checker, used to get input stores based on CFG blocks. */ - private final ResourceLeakAnalysis analysis; + private final RLCCalledMethodsAnalysis analysis; /** True if -AnoLightweightOwnership was passed on the command line. */ private final boolean noLightweightOwnership; @@ -185,7 +187,7 @@ class MustCallConsistencyAnalyzer { private final boolean countMustCall; /** A description for how a method might exit. */ - /*package-private*/ enum MethodExitKind { + public enum MethodExitKind { /** The method exits normally by returning. */ NORMAL_RETURN, @@ -204,8 +206,8 @@ class MustCallConsistencyAnalyzer { * might have a must-call obligation. Each Obligation is a pair of a set of resource aliases and * their must-call obligation. Must-call obligations are tracked by the {@link MustCallChecker} * and are accessed by looking up the type(s) in its type system of the resource aliases contained - * in each {@code Obligation} using {@link #getMustCallMethods(ResourceLeakAnnotatedTypeFactory, - * CFStore)}. + * in each {@code Obligation} using {@link + * #getMustCallMethods(RLCCalledMethodsAnnotatedTypeFactory, CFStore)}. * *

An Obligation might not matter on all paths out of a method. For instance, after a * constructor assigns a resource to an {@link Owning} field, the resource only needs to be closed @@ -328,7 +330,7 @@ public boolean derivedFromMustCallAlias() { * resource alias of this in the Must Call store is MustCallUnknown) */ public @Nullable Map> getMustCallMethods( - ResourceLeakAnnotatedTypeFactory rlAtf, @Nullable CFStore mcStore) { + RLCCalledMethodsAnnotatedTypeFactory rlAtf, @Nullable CFStore mcStore) { Map> result = new HashMap<>(this.resourceAliases.size()); MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); @@ -549,14 +551,13 @@ public String stringForErrorMessage() { * instantiate a new consistency analyzer using this constructor and then call {@link * #analyze(ControlFlowGraph)}. * - * @param typeFactory the type factory + * @param rlc the resource leak checker * @param analysis the analysis from the type factory. Usually this would have protected access, * so this constructor cannot get it directly. */ - /*package-private*/ MustCallConsistencyAnalyzer( - ResourceLeakAnnotatedTypeFactory typeFactory, ResourceLeakAnalysis analysis) { - this.typeFactory = typeFactory; - this.checker = (ResourceLeakChecker) typeFactory.getChecker(); + public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, RLCCalledMethodsAnalysis analysis) { + this.cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) analysis.getTypeFactory(); + this.checker = rlc; this.analysis = analysis; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); @@ -582,7 +583,7 @@ public String stringForErrorMessage() { */ // TODO: This analysis is currently implemented directly using a worklist; in the future, it // should be rewritten to use the dataflow framework of the Checker Framework. - /*package-private*/ void analyze(ControlFlowGraph cfg) { + public void analyze(ControlFlowGraph cfg) { // The `visited` set contains everything that has been added to the worklist, even if it has // not yet been removed and analyzed. Set visited = new HashSet<>(); @@ -614,8 +615,8 @@ private void updateObligationsForInvocation( Set obligations, Node node, @Nullable TypeMirror exceptionType) { removeObligationsAtOwnershipTransferToParameters(obligations, node, exceptionType); if (node instanceof MethodInvocationNode - && typeFactory.canCreateObligations() - && typeFactory.hasCreatesMustCallFor((MethodInvocationNode) node)) { + && cmAtf.canCreateObligations() + && cmAtf.hasCreatesMustCallFor((MethodInvocationNode) node)) { checkCreatesMustCallForInvocation(obligations, (MethodInvocationNode) node); // Count calls to @CreatesMustCallFor methods as creating new resources. Doing so could // result in slightly over-counting, because @CreatesMustCallFor doesn't guarantee that @@ -627,7 +628,7 @@ private void updateObligationsForInvocation( return; } - if (typeFactory.declaredTypeHasMustCall(node.getTree())) { + if (cmAtf.declaredTypeHasMustCall(node.getTree())) { // The incrementNumMustCall call above increments the count for the target of the // @CreatesMustCallFor annotation. By contrast, this call increments the count for the // return value of the method (which can't be the target of the annotation, because our @@ -662,10 +663,10 @@ private void updateObligationsForInvocation( private void checkCreatesMustCallForInvocation( Set obligations, MethodInvocationNode node) { - TreePath currentPath = typeFactory.getPath(node.getTree()); + TreePath currentPath = cmAtf.getPath(node.getTree()); List cmcfExpressions = CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( - node, typeFactory, typeFactory); + node, cmAtf, cmAtf); List missing = new ArrayList<>(0); for (JavaExpression expression : cmcfExpressions) { if (!isValidCreatesMustCallForExpression(obligations, expression, currentPath)) { @@ -724,13 +725,13 @@ private boolean isValidCreatesMustCallForExpression( Set obligations, JavaExpression expression, TreePath invocationPath) { if (expression instanceof FieldAccess) { Element elt = ((FieldAccess) expression).getField(); - if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { + if (!noLightweightOwnership && cmAtf.hasOwning(elt)) { // The expression is an Owning field. This satisfies case 1. return true; } } else if (expression instanceof LocalVariable) { Element elt = ((LocalVariable) expression).getElement(); - if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { + if (!noLightweightOwnership && cmAtf.hasOwning(elt)) { // The expression is an Owning formal parameter. Note that this cannot actually // be a local variable (despite expressions's type being LocalVariable) because // the @Owning annotation can only be written on methods, parameters, and fields; @@ -769,10 +770,9 @@ private boolean isValidCreatesMustCallForExpression( return false; } ExecutableElement callerMethodElt = TreeUtils.elementFromDeclaration(callerMethodTree); - MustCallAnnotatedTypeFactory mcAtf = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + MustCallAnnotatedTypeFactory mcAtf = cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); List callerCmcfValues = - ResourceLeakVisitor.getCreatesMustCallForValues(callerMethodElt, mcAtf, typeFactory); + RLCCalledMethodsVisitor.getCreatesMustCallForValues(callerMethodElt, mcAtf, cmAtf); if (callerCmcfValues.isEmpty()) { return false; } @@ -835,7 +835,7 @@ private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosin // Only track the result of the call if there is a temporary variable for the call node // (because if there is no temporary, then the invocation must produce an untrackable value, // such as a primitive type). - LocalVariableNode tmpVar = typeFactory.getTempVarForNode(node); + LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); if (tmpVar == null) { return; } @@ -844,8 +844,7 @@ private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosin // position. List mustCallAliases = getMustCallAliasArgumentNodes(node); // If call returns @This, add the receiver to mustCallAliases. - if (node instanceof MethodInvocationNode - && typeFactory.returnsThis((MethodInvocationTree) tree)) { + if (node instanceof MethodInvocationNode && cmAtf.returnsThis((MethodInvocationTree) tree)) { mustCallAliases.add( removeCastsAndGetTmpVarIfPresent( ((MethodInvocationNode) node).getTarget().getReceiver())); @@ -932,8 +931,8 @@ public boolean shouldTrackInvocationResult( ExecutableElement executableElement = TreeUtils.elementFromUse(newClassTree); TypeElement typeElt = TypesUtils.getTypeElement(ElementUtils.getType(executableElement)); return typeElt == null - || !typeFactory.hasEmptyMustCallValue(typeElt) - || !typeFactory.hasEmptyMustCallValue(newClassTree); + || !cmAtf.hasEmptyMustCallValue(typeElt) + || !cmAtf.hasEmptyMustCallValue(newClassTree); } // Now callTree.getKind() == Tree.Kind.METHOD_INVOCATION. @@ -1030,7 +1029,7 @@ private void removeObligationsAtOwnershipTransferToParameters( // check if parameter has an @Owning annotation VariableElement parameter = parameters.get(i); - if (typeFactory.hasOwning(parameter)) { + if (cmAtf.hasOwning(parameter)) { Obligation localObligation = getObligationForVar(obligations, local); // Passing to an owning parameter is not sufficient to resolve the // obligation created from a MustCallAlias parameter, because the @@ -1074,7 +1073,7 @@ private void updateObligationsForOwningReturn( * @return the temporary for node, or node if no temporary exists */ /*package-private*/ Node getTempVarOrNode(Node node) { - Node temp = typeFactory.getTempVarForNode(node); + Node temp = cmAtf.getTempVarForNode(node); if (temp != null) { return temp; } @@ -1101,7 +1100,7 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { // not be transferred. MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); - return !typeFactory.hasNotOwning(executableElement); + return !cmAtf.hasNotOwning(executableElement); } return false; } @@ -1129,12 +1128,10 @@ private void updateObligationsForAssignment( // Ownership transfer to @Owning field. if (lhsElement.getKind() == ElementKind.FIELD) { - boolean isOwningField = !noLightweightOwnership && typeFactory.hasOwning(lhsElement); + boolean isOwningField = !noLightweightOwnership && cmAtf.hasOwning(lhsElement); // Check that the must-call obligations of the lhs have been satisfied, if the field is // non-final and owning. - if (isOwningField - && typeFactory.canCreateObligations() - && !ElementUtils.isFinal(lhsElement)) { + if (isOwningField && cmAtf.canCreateObligations() && !ElementUtils.isFinal(lhsElement)) { checkReassignmentToField(obligations, assignmentNode); } @@ -1142,7 +1139,7 @@ private void updateObligationsForAssignment( // (When obligation creation is turned off, non-final fields cannot take ownership.) if (isOwningField && rhs instanceof LocalVariableNode - && (typeFactory.canCreateObligations() || ElementUtils.isFinal(lhsElement))) { + && (cmAtf.canCreateObligations() || ElementUtils.isFinal(lhsElement))) { LocalVariableNode rhsVar = (LocalVariableNode) rhs; @@ -1207,12 +1204,11 @@ private void updateObligationsForAssignment( * @return true iff element has no more than 1 owning field */ private boolean hasAtMostOneOwningField(TypeElement element) { - List fields = - ElementUtils.getAllFieldsIn(element, typeFactory.getElementUtils()); + List fields = ElementUtils.getAllFieldsIn(element, cmAtf.getElementUtils()); // Has an owning field already been encountered? boolean hasOwningField = false; for (VariableElement field : fields) { - if (typeFactory.hasOwning(field)) { + if (cmAtf.hasOwning(field)) { if (hasOwningField) { return false; } else { @@ -1344,7 +1340,7 @@ private void removeObligationsContainingVar( * @param lhsVar the left-hand side variable for the pseudo-assignment * @param rhs the right-hand side for the pseudo-assignment, which must have been converted to a * temporary variable (via a call to {@link - * ResourceLeakAnnotatedTypeFactory#getTempVarForNode}) + * RLCCalledMethodsAnnotatedTypeFactory#getTempVarForNode}) */ /*package-private*/ void updateObligationsForPseudoAssignment( Set obligations, Node node, LocalVariableNode lhsVar, Node rhs) { @@ -1380,14 +1376,12 @@ private void removeObligationsContainingVar( // cases, use the tree associated with the temp var for the resource alias, // as that is the tree where errors should be reported. Tree treeForAlias = - typeFactory.isTempVar(lhsVar) - ? typeFactory.getTreeForTempVar(lhsVar) - : node.getTree(); + cmAtf.isTempVar(lhsVar) ? cmAtf.getTreeForTempVar(lhsVar) : node.getTree(); aliasForAssignment = new ResourceAlias(new LocalVariable(lhsVar), treeForAlias); } newResourceAliasesForObligation.add(aliasForAssignment); // Remove temp vars from tracking once they are assigned to another location. - if (typeFactory.isTempVar(rhsVar)) { + if (cmAtf.isTempVar(rhsVar)) { ResourceAlias aliasForRhs = obligation.getResourceAlias(rhsVar); if (aliasForRhs != null) { newResourceAliasesForObligation.remove(aliasForRhs); @@ -1405,10 +1399,10 @@ private void removeObligationsContainingVar( // Because the last reference to the resource has been overwritten, check the // must-call obligation. MustCallAnnotatedTypeFactory mcAtf = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); checkMustCall( obligation, - typeFactory.getStoreBefore(node), + cmAtf.getStoreBefore(node), mcAtf.getStoreBefore(node), "variable overwritten by assignment " + node.getTree()); replacements.put(obligation, null); @@ -1459,7 +1453,7 @@ private void checkReassignmentToField(Set obligations, AssignmentNod // It might be possible to exploit the CFG structure to find the enclosing // method (rather than using the path, as below), because if a method is being // analyzed then it should be the root of the CFG (I think). - TreePath currentPath = typeFactory.getPath(node.getTree()); + TreePath currentPath = cmAtf.getPath(node.getTree()); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); if (enclosingMethodTree == null) { @@ -1480,7 +1474,7 @@ private void checkReassignmentToField(Set obligations, AssignmentNod } else { // Issue an error if the field has a non-empty must-call type. MustCallAnnotatedTypeFactory mcTypeFactory = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); AnnotationMirror mcAnno = mcTypeFactory.getAnnotatedType(lhs.getElement()).getPrimaryAnnotation(MustCall.class); List mcValues = @@ -1541,7 +1535,7 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) if (arguments.size() == parameters.size()) { for (int i = 0; i < arguments.size(); i++) { VariableElement param = parameters.get(i); - if (typeFactory.hasOwning(param)) { + if (cmAtf.hasOwning(param)) { Node argument = arguments.get(i); if (argument.equals(lhs)) { return; @@ -1561,7 +1555,7 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) } MustCallAnnotatedTypeFactory mcTypeFactory = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); // Get the Must Call type for the field. If there's info about this field in the store, use // that. Otherwise, use the declared type of the field @@ -1594,13 +1588,13 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) // Get the store before the RHS rather than the assignment node, because the CFG always has // the RHS first. If the RHS has side-effects, then the assignment node's store will have // had its inferred types erased. - AccumulationStore cmStoreBefore = typeFactory.getStoreBefore(rhs); + AccumulationStore cmStoreBefore = cmAtf.getStoreBefore(rhs); AccumulationValue cmValue = cmStoreBefore == null ? null : cmStoreBefore.getValue(lhs); AnnotationMirror cmAnno = null; if (cmValue != null) { // When store contains the lhs Set accumulatedValues = cmValue.getAccumulatedValues(); if (accumulatedValues != null) { // type variable or wildcard type - cmAnno = typeFactory.createCalledMethods(accumulatedValues.toArray(new String[0])); + cmAnno = cmAtf.createCalledMethods(accumulatedValues.toArray(new String[0])); } else { for (AnnotationMirror anno : cmValue.getAnnotations()) { if (AnnotationUtils.areSameByName( @@ -1611,7 +1605,7 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) } } if (cmAnno == null) { - cmAnno = typeFactory.top; + cmAnno = cmAtf.top; } if (!calledMethodsSatisfyMustCall(mcValues, cmAnno)) { VariableElement lhsElement = TreeUtils.variableElementFromTree(lhs.getTree()); @@ -1651,11 +1645,10 @@ private void checkEnclosingMethodIsCreatesMustCallFor( return; } ExecutableElement enclosingMethodElt = TreeUtils.elementFromDeclaration(enclosingMethod); - MustCallAnnotatedTypeFactory mcAtf = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + MustCallAnnotatedTypeFactory mcAtf = cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); List cmcfValues = - ResourceLeakVisitor.getCreatesMustCallForValues(enclosingMethodElt, mcAtf, typeFactory); + RLCCalledMethodsVisitor.getCreatesMustCallForValues(enclosingMethodElt, mcAtf, cmAtf); if (cmcfValues.isEmpty()) { checker.reportError( @@ -1731,14 +1724,14 @@ private List getMustCallAliasArgumentNodes(Node callNode) { Preconditions.checkArgument( callNode instanceof MethodInvocationNode || callNode instanceof ObjectCreationNode); List result = new ArrayList<>(); - if (!typeFactory.hasMustCallAlias(callNode.getTree())) { + if (!cmAtf.hasMustCallAlias(callNode.getTree())) { return result; } List args = getArgumentsOfInvocation(callNode); List parameters = getParametersOfInvocation(callNode); for (int i = 0; i < args.size(); i++) { - if (typeFactory.hasMustCallAlias(parameters.get(i))) { + if (cmAtf.hasMustCallAlias(parameters.get(i))) { result.add(removeCastsAndGetTmpVarIfPresent(args.get(i))); } } @@ -1823,7 +1816,7 @@ private boolean shouldTrackReturnType(MethodInvocationNode node) { } MethodInvocationTree methodInvocationTree = node.getTree(); ExecutableElement executableElement = TreeUtils.elementFromUse(methodInvocationTree); - if (typeFactory.hasMustCallAlias(executableElement)) { + if (cmAtf.hasMustCallAlias(executableElement)) { // assume tracking is required return true; } @@ -1835,18 +1828,19 @@ private boolean shouldTrackReturnType(MethodInvocationNode node) { TypeElement typeElt = TypesUtils.getTypeElement(type); // no need to track if type has no possible @MustCall obligation if (typeElt != null - && typeFactory.hasEmptyMustCallValue(typeElt) - && typeFactory.hasEmptyMustCallValue(methodInvocationTree)) { + && cmAtf.hasEmptyMustCallValue(typeElt) + && cmAtf.hasEmptyMustCallValue(methodInvocationTree)) { return false; } // check for absence of @NotOwning annotation - return !typeFactory.hasNotOwning(executableElement); + return !cmAtf.hasNotOwning(executableElement); } /** * Get all successor blocks for some block, except for those corresponding to ignored exception - * types. See {@link ResourceLeakAnalysis#isIgnoredExceptionType(TypeMirror)}. Each exceptional - * successor is paired with the type of exception that leads to it, for use in error messages. + * types. See {@link RLCCalledMethodsAnalysis#isIgnoredExceptionType(TypeMirror)}. Each + * exceptional successor is paired with the type of exception that leads to it, for use in error + * messages. * * @param block input block * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for @@ -2017,7 +2011,7 @@ private void propagateObligationsToSuccessorBlock( if (successor.getType() == BlockType.SPECIAL_BLOCK /* special blocks are exit blocks */ || obligationGoesOutOfScopeBeforeSuccessor) { MustCallAnnotatedTypeFactory mcAtf = - typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); // If successor is an exceptional successor, and Obligation represents the // temporary variable for currentBlock's node, do not propagate or do a @@ -2033,7 +2027,7 @@ private void propagateObligationsToSuccessorBlock( // Whether exceptionType is null captures the logic of both of these cases. if (exceptionType != null) { Node exceptionalNode = NodeUtils.removeCasts(((ExceptionBlock) currentBlock).getNode()); - LocalVariableNode tmpVarForExcNode = typeFactory.getTempVarForNode(exceptionalNode); + LocalVariableNode tmpVarForExcNode = cmAtf.getTempVarForNode(exceptionalNode); if (tmpVarForExcNode != null && obligation.resourceAliases.size() == 1 && obligation.canBeSatisfiedThrough(tmpVarForExcNode)) { @@ -2113,7 +2107,7 @@ private void propagateObligationsToSuccessorBlock( if (cmStoreAfter.containsKey(last)) { cmStore = cmStoreAfter.get(last); } else { - cmStore = typeFactory.getStoreAfter(last); + cmStore = cmAtf.getStoreAfter(last); cmStoreAfter.put(last, cmStore); } // If this is an exceptional block, check the MC store beforehand to avoid @@ -2152,13 +2146,13 @@ private void propagateObligationsToSuccessorBlock( } /** - * Gets the store propagated by the {@link ResourceLeakAnalysis} (containing called methods + * Gets the store propagated by the {@link RLCCalledMethodsAnalysis} (containing called methods * information) along a particular CFG edge during local type inference. The source {@link Block} * of the edge must contain no {@link Node}s. * * @param currentBlock source block of the CFG edge. Must contain no {@link Node}s. * @param successor target block of the CFG edge. - * @return store propagated by the {@link ResourceLeakAnalysis} along the CFG edge. + * @return store propagated by the {@link RLCCalledMethodsAnalysis} along the CFG edge. */ private AccumulationStore getStoreForEdgeFromEmptyBlock(Block currentBlock, Block successor) { switch (currentBlock.getType()) { @@ -2203,7 +2197,7 @@ private boolean isInvocationOfCreatesMustCallForMethod(Node node) { return false; } MethodInvocationNode miNode = (MethodInvocationNode) node; - return typeFactory.hasCreatesMustCallFor(miNode); + return cmAtf.hasCreatesMustCallFor(miNode); } /** @@ -2220,9 +2214,9 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { Set result = new LinkedHashSet<>(1); for (VariableTree param : method.getParameters()) { VariableElement paramElement = TreeUtils.elementFromDeclaration(param); - boolean hasMustCallAlias = typeFactory.hasMustCallAlias(paramElement); + boolean hasMustCallAlias = cmAtf.hasMustCallAlias(paramElement); if (hasMustCallAlias - || (typeFactory.declaredTypeHasMustCall(param) + || (cmAtf.declaredTypeHasMustCall(param) && !noLightweightOwnership && paramElement.getAnnotation(Owning.class) != null)) { result.add( @@ -2292,8 +2286,7 @@ private static boolean varTrackedInObligations( private void checkMustCall( Obligation obligation, AccumulationStore cmStore, CFStore mcStore, String outOfScopeReason) { - Map> mustCallValues = - obligation.getMustCallMethods(typeFactory, mcStore); + Map> mustCallValues = obligation.getMustCallMethods(cmAtf, mcStore); // Optimization: if mustCallValues is null, always issue a warning (there is no way to // satisfy the check). A null mustCallValue occurs when the type is top @@ -2337,7 +2330,7 @@ private void checkMustCall( if (cmValue != null) { // When store contains the lhs Set accumulatedValues = cmValue.getAccumulatedValues(); if (accumulatedValues != null) { // type variable or wildcard type - cmAnno = typeFactory.createCalledMethods(accumulatedValues.toArray(new String[0])); + cmAnno = cmAtf.createCalledMethods(accumulatedValues.toArray(new String[0])); } else { for (AnnotationMirror anno : cmValue.getAnnotations()) { if (AnnotationUtils.areSameByName( @@ -2350,10 +2343,7 @@ private void checkMustCall( } } if (cmAnno == null) { - cmAnno = - typeFactory - .getAnnotatedType(alias.element) - .getEffectiveAnnotationInHierarchy(typeFactory.top); + cmAnno = cmAtf.getAnnotatedType(alias.element).getEffectiveAnnotationInHierarchy(cmAtf.top); } if (calledMethodsSatisfyMustCall(mustCallValuesForAlias, cmAnno)) { @@ -2444,10 +2434,8 @@ private void incrementMustCallImpl(TypeMirror type) { // Create this annotation and use a subtype test because there's no guarantee that // cmAnno is actually an instance of CalledMethods: it could be CMBottom or CMPredicate. AnnotationMirror cmAnnoForMustCallMethods = - typeFactory.createCalledMethods(mustCallValues.toArray(new String[0])); - return typeFactory - .getQualifierHierarchy() - .isSubtypeQualifiersOnly(cmAnno, cmAnnoForMustCallMethods); + cmAtf.createCalledMethods(mustCallValues.toArray(new String[0])); + return cmAtf.getQualifierHierarchy().isSubtypeQualifiersOnly(cmAnno, cmAnnoForMustCallMethods); } /** @@ -2474,7 +2462,7 @@ private static void propagate( * @param mustCallVal the list of must-call strings * @return a formatted string */ - /*package-private*/ static String formatMissingMustCallMethods(List mustCallVal) { + public static String formatMissingMustCallMethods(List mustCallVal) { int size = mustCallVal.size(); if (size == 0) { throw new TypeSystemError("empty mustCallVal " + mustCallVal); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java index c1a7071d673f..2721931722b0 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java @@ -33,6 +33,7 @@ import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.MethodExitKind; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.ResourceAlias; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.common.wholeprograminference.WholeProgramInference; @@ -69,9 +70,9 @@ * *

Each instance of this class corresponds to a single control flow graph (CFG), typically * representing a method. The entry method of this class is {@link - * #runMustCallInference(ResourceLeakAnnotatedTypeFactory, ControlFlowGraph, + * #runMustCallInference(RLCCalledMethodsAnnotatedTypeFactory, ControlFlowGraph, * MustCallConsistencyAnalyzer)}, invoked from the {@link - * ResourceLeakAnnotatedTypeFactory#postAnalyze} method when Whole Program Inference is enabled. + * RLCCalledMethodsAnnotatedTypeFactory#postAnalyze} method when Whole Program Inference is enabled. * *

The algorithm determines if the @MustCall obligation of a field is satisfied along some path * leading to the regular exit point of the method. If the obligation is satisfied, the algorithm @@ -120,7 +121,7 @@ public class MustCallInference { /** * The type factory for the Resource Leak Checker, which is used to access the Must Call Checker. */ - private final ResourceLeakAnnotatedTypeFactory resourceLeakAtf; + private final RLCCalledMethodsAnnotatedTypeFactory resourceLeakAtf; /** The MustCallConsistencyAnalyzer. */ private final MustCallConsistencyAnalyzer mcca; @@ -172,7 +173,7 @@ public class MustCallInference { * @param mcca the MustCallConsistencyAnalyzer */ /*package-private*/ MustCallInference( - ResourceLeakAnnotatedTypeFactory resourceLeakAtf, + RLCCalledMethodsAnnotatedTypeFactory resourceLeakAtf, ControlFlowGraph cfg, MustCallConsistencyAnalyzer mcca) { this.resourceLeakAtf = resourceLeakAtf; @@ -198,15 +199,15 @@ public class MustCallInference { /** * Creates a MustCallInference instance and runs the inference algorithm. This method is called by - * the {@link ResourceLeakAnnotatedTypeFactory#postAnalyze} method if Whole Program Inference is - * enabled. + * the {@link RLCCalledMethodsAnnotatedTypeFactory#postAnalyze} method if Whole Program Inference + * is enabled. * * @param resourceLeakAtf the type factory * @param cfg the control flow graph of the method to check * @param mcca the MustCallConsistencyAnalyzer */ - /*package-private*/ static void runMustCallInference( - ResourceLeakAnnotatedTypeFactory resourceLeakAtf, + public static void runMustCallInference( + RLCCalledMethodsAnnotatedTypeFactory resourceLeakAtf, ControlFlowGraph cfg, MustCallConsistencyAnalyzer mcca) { MustCallInference mustCallInferenceLogic = new MustCallInference(resourceLeakAtf, cfg, mcca); @@ -248,7 +249,7 @@ private void runInference() { // before the checking phase. However, calling // updateObligationsWithInvocationResult() will not have any side effects on the // outcome of the Resource Leak Checker. This is because the inference occurs within - // the postAnalyze method of the ResourceLeakAnnotatedTypeFactory, once the + // the postAnalyze method of the RLCCalledMethodsAnnotatedTypeFactory, once the // consistency analyzer has completed its process. if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { if (mcca.shouldTrackInvocationResult(obligations, node, true)) { @@ -393,7 +394,7 @@ private void addOwningToParam(int index) { * @return true if the field is an owning candidate, false otherwise */ private boolean isFieldOwningCandidate( - ResourceLeakAnnotatedTypeFactory resourceLeakAtf, Element field) { + RLCCalledMethodsAnnotatedTypeFactory resourceLeakAtf, Element field) { AnnotationMirror mustCallAnnotation = resourceLeakAtf.getMustCallAnnotation(field); if (mustCallAnnotation == null) { // Indicates @MustCallUnknown. We want to conservatively avoid inferring an @Owning diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index da21e5e08f54..34ccf6488235 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableSet; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; @@ -10,21 +11,29 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; -import org.checkerframework.checker.calledmethods.CalledMethodsChecker; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.mustcall.MustCallChecker; -import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.source.SupportedOptions; /** - * The entry point for the Resource Leak Checker. This checker is a modifed {@link - * CalledMethodsChecker} that checks that the must-call obligations of each expression (as computed - * via the {@link org.checkerframework.checker.mustcall.MustCallChecker} have been fulfilled. + * The entry point for the Resource Leak Checker. This checker only counts the number of {@link + * org.checkerframework.checker.mustcall.qual.MustCall} annotations and defines a set of ignored + * exceptions. This checker calls the {@link RLCCalledMethodsChecker} as a direct subchecker, which + * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards traverses the cfg + * to check whether all MustCall obligations are fulfilled. + * + *

The checker hierarchy is: this "empty" RLC → RLCCalledMethodsChecker → + * MustCallChecker + * + *

The MustCallChecker is a subchecker of the RLCCm checker (instead of a sibling), since we want + * them to operate on the same cfg (so we can get both a CM and MC store for a given cfg block), + * which only works if one of them is a subchecker of the other. */ @SupportedOptions({ "permitStaticOwning", @@ -38,7 +47,7 @@ ResourceLeakChecker.ENABLE_RETURNS_RECEIVER }) @StubFiles("IOUtils.astub") -public class ResourceLeakChecker extends CalledMethodsChecker { +public class ResourceLeakChecker extends AggregateChecker { /** Creates a ResourceLeakChecker. */ public ResourceLeakChecker() {} @@ -144,23 +153,13 @@ public ResourceLeakChecker() {} private @MonotonicNonNull SetOfTypes ignoredExceptions = null; @Override - protected Set> getImmediateSubcheckerClasses() { - Set> checkers = super.getImmediateSubcheckerClasses(); - - if (this.processingEnv.getOptions().containsKey(MustCallChecker.NO_CREATES_MUSTCALLFOR)) { - checkers.add(MustCallNoCreatesMustCallForChecker.class); - } else { - checkers.add(MustCallChecker.class); - } + protected Set> getSupportedCheckers() { + Set> checkers = new LinkedHashSet<>(1); + checkers.add(RLCCalledMethodsChecker.class); return checkers; } - @Override - protected BaseTypeVisitor createSourceVisitor() { - return new ResourceLeakVisitor(this); - } - @Override public void reportError( @Nullable Object source, @CompilerMessageKey String messageKey, Object... args) { @@ -191,9 +190,10 @@ public void typeProcessingOver() { * Disable the Returns Receiver Checker unless it has been explicitly enabled with the {@link * #ENABLE_RETURNS_RECEIVER} option. */ - @Override protected boolean isReturnsReceiverDisabled() { - return !hasOption(ENABLE_RETURNS_RECEIVER) || super.isReturnsReceiverDisabled(); + RLCCalledMethodsChecker rlccmc = + (RLCCalledMethodsChecker) ResourceLeakUtils.getChecker(RLCCalledMethodsChecker.class, this); + return !hasOption(ENABLE_RETURNS_RECEIVER) || rlccmc.isReturnsReceiverDisabled(); } /** @@ -306,4 +306,12 @@ protected SetOfTypes parseIgnoredExceptions(String ignoredExceptionsOptionValue) } return types.getDeclaredType(elem); } + + // @Override + // public NavigableSet getSuppressWarningsPrefixes() { + // NavigableSet result = super.getSuppressWarningsPrefixes(); + // result.add("builder"); + // return result; + // } + } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java new file mode 100644 index 000000000000..2d4fa660fa79 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -0,0 +1,362 @@ +package org.checkerframework.checker.resourceleak; + +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +/** + * Collection of static utility functions related to the various (sub-) checkers within the + * ResourceLeakChecker. + */ +public class ResourceLeakUtils { + + /** List of checker names associated with the Resource Leak Checker. */ + public static List rlcCheckers = + new ArrayList<>( + Arrays.asList( + ResourceLeakChecker.class.getCanonicalName(), + RLCCalledMethodsChecker.class.getCanonicalName(), + MustCallChecker.class.getCanonicalName(), + MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); + + /** + * Returns the type factory corresponding to the desired checker class within the + * ResourceLeakChecker given a checker part of the ResourceLeakChecker. + * + * @param targetClass the desired checker class + * @param referenceChecker the current checker + * @return the type factory of the desired class + */ + public static @NonNull AnnotatedTypeFactory getTypeFactory( + Class targetClass, SourceChecker referenceChecker) { + BaseTypeChecker targetChecker = (BaseTypeChecker) getChecker(targetClass, referenceChecker); + return targetChecker.getTypeFactory(); + } + + /** + * Returns the type factory corresponding to the desired checker class within the + * ResourceLeakChecker given a type factory part of the ResourceLeakChecker. + * + * @param targetClass the desired checker class + * @param referenceAtf the current atf + * @return the type factory of the desired class + */ + public static @NonNull AnnotatedTypeFactory getTypeFactory( + Class targetClass, AnnotatedTypeFactory referenceAtf) { + if (!rlcCheckers.contains(targetClass.getCanonicalName())) { + throw new IllegalArgumentException( + "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " + + targetClass.getCanonicalName()); + } + return ((BaseTypeChecker) getChecker(targetClass, referenceAtf.getChecker())).getTypeFactory(); + } + + /** + * Returns the checker of the desired class within the ResourceLeakChecker given a type factory + * part of the ResourceLeakChecker. + * + * @param targetClass the desired checker class + * @param referenceAtf the current atf + * @return the checker of the desired class + */ + public static @NonNull SourceChecker getChecker( + Class targetClass, AnnotatedTypeFactory referenceAtf) { + if (!rlcCheckers.contains(targetClass.getCanonicalName())) { + throw new IllegalArgumentException( + "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " + + targetClass.getCanonicalName()); + } + return getChecker(targetClass, referenceAtf.getChecker()); + } + + public static @NonNull ResourceLeakChecker getResourceLeakChecker( + AnnotatedTypeFactory referenceAtf) { + if (referenceAtf == null) { + throw new IllegalArgumentException("Argument referenceAtf cannot be null"); + } else { + return getResourceLeakChecker(referenceAtf.getChecker()); + } + } + + public static @NonNull ResourceLeakChecker getResourceLeakChecker( + SourceChecker referenceChecker) { + if (referenceChecker == null) { + throw new IllegalArgumentException("Argument referenceChecker cannot be null"); + } + + String className = referenceChecker.getClass().getCanonicalName(); + if ("ResourceLeakChecker".equals(className)) { + return (ResourceLeakChecker) referenceChecker; + } else if ("RLCCalledMethodsChecker".equals(className) + || "MustCallChecker".equals(className) + || "MustCallNoCreatesMustCallForChecker".equals(className)) { + return getResourceLeakChecker(referenceChecker.getParentChecker()); + } else { + throw new IllegalArgumentException( + "Argument referenceChecker to ResourceLeakUtils#getResourceLeakChecker(referenceChecker) expected to be an RLC checker but is " + + className); + } + } + + public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( + AnnotatedTypeFactory referenceAtf) { + if (referenceAtf == null) { + throw new IllegalArgumentException("Argument referenceAtf cannot be null"); + } else { + return getRLCCalledMethodsChecker(referenceAtf.getChecker()); + } + } + + public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( + SourceChecker referenceChecker) { + if (referenceChecker == null) { + throw new IllegalArgumentException("Argument referenceChecker cannot be null"); + } + + String className = referenceChecker.getClass().getCanonicalName(); + if ("RLCCalledMethodsChecker".equals(className)) { + return (RLCCalledMethodsChecker) referenceChecker; + } else if ("ResourceLeakChecker".equals(className)) { + return getRLCCalledMethodsChecker( + referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); + } else if ("MustCallChecker".equals(className) + || "MustCallNoCreatesMustCallForChecker".equals(className)) { + return getRLCCalledMethodsChecker(referenceChecker.getParentChecker()); + } else { + throw new IllegalArgumentException( + "Argument referenceChecker to ResourceLeakUtils#getRLCCalledMethodsChecker(referenceChecker) expected to be an RLC checker but is " + + className); + } + } + + /** + * Returns the checker of the desired class given a checker part of the RLC. Both the targetClass + * and reference checker must be checkers from the RLC ecosystem, as defined by {@code + * this.rlcCheckers}. + * + * @param targetClass the desired checker class + * @param referenceChecker the current checker + * @return the checker of the desired class + * @throws IllegalArgumentException when either of the arguments is not one of the RLC checkers + */ + public static @NonNull SourceChecker getChecker( + Class targetClass, SourceChecker referenceChecker) { + if (!rlcCheckers.contains(targetClass.getCanonicalName())) { + throw new IllegalArgumentException( + "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " + + targetClass.getCanonicalName()); + } + Class refClass = referenceChecker.getClass(); + if (refClass == targetClass) { + // base case -- we found the desired checker + return referenceChecker; + } else if (refClass == MustCallChecker.class) { + return getChecker(targetClass, referenceChecker.getParentChecker()); + } else if (refClass == ResourceLeakChecker.class) { + return getChecker(targetClass, referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); + } else if (refClass == RLCCalledMethodsChecker.class) { + if (targetClass == MustCallChecker.class) { + MustCallChecker mcc = referenceChecker.getSubchecker(MustCallChecker.class); + return mcc != null + ? mcc + : referenceChecker.getSubchecker(MustCallNoCreatesMustCallForChecker.class); + } else { + return getChecker(targetClass, referenceChecker.getParentChecker()); + } + } else { + throw new IllegalArgumentException( + "Argument referenceChecker to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " + + refClass.getCanonicalName()); + } + } + + /** + * Returns the list of mustcall obligations for the given {@code TypeMirror} upper bound (either + * the type variable itself if it is concrete or the upper bound if its a wildcard or generic). + * + *

If the type variable has no upper bound, for instance if it is a wildcard with no extends + * clause the method returns null + * + * @param type the {@code TypeMirror} + * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type + * @return the list of mustcall obligations for the upper bound of {@code type} or null if the + * upper bound is null. + */ + public static @Nullable List getMcValues( + TypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { + if (type instanceof TypeVariable) { + // a generic - replace with upper bound and return null if it has no upper bound + type = ((TypeVariable) type).getUpperBound(); + if (type == null) { + return null; + } + } else if (type instanceof WildcardType) { + // a wildcard - replace with upper bound and return null if it has no upper bound + type = ((WildcardType) type).getExtendsBound(); + if (type == null) { + return null; + } + } + TypeElement typeElement = TypesUtils.getTypeElement(type); + AnnotationMirror imcAnnotation = + mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); + AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); + Set mcValues = new HashSet<>(); + if (mcAnnotation != null) { + mcValues.addAll( + AnnotationUtils.getElementValueArray( + mcAnnotation, mcAtf.getMustCallValueElement(), String.class)); + } + if (imcAnnotation != null) { + mcValues.addAll( + AnnotationUtils.getElementValueArray( + imcAnnotation, mcAtf.getInheritableMustCallValueElement(), String.class)); + } + return new ArrayList<>(mcValues); + } + + /** + * Returns whether the given {@link TypeMirror} is an instance of a collection (subclass). This is + * determined by getting the class of the TypeMirror and checking whether it is assignable from + * Collection. + * + * @param type the TypeMirror + * @return whether type is an instance of a collection (subclass) + */ + public static boolean isCollection(TypeMirror type) { + if (type == null) return false; + Class elementRawType = TypesUtils.getClassFromType(type); + if (elementRawType == null) return false; + return Collection.class.isAssignableFrom(elementRawType); + } + + /** + * Returns whether the given Element is a java.util.Collection type by checking whether the raw + * type of the element is assignable from java.util.Collection. Returns false if element is null, + * or has no valid type. + * + * @param element the element + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given element is a Java.util.Collection type + */ + public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { + if (element == null) return false; + AnnotatedTypeMirror elementTypeMirror = atf.getAnnotatedType(element).getErased(); + if (elementTypeMirror == null || elementTypeMirror.getUnderlyingType() == null) return false; + return isCollection(elementTypeMirror.getUnderlyingType()); + } + + /** + * Returns whether the given Tree is a java.util.Collection type by checking whether the raw type + * of the element is assignable from java.util.Collection. Returns false if tree is null, or has + * no valid type. + * + * @param tree the tree + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given Tree is a Java.util.Collection type + */ + public static boolean isCollection(Tree tree, AnnotatedTypeFactory atf) { + if (tree == null) return false; + Element element = TreeUtils.elementFromTree(tree); + return isCollection(element, atf); + } + + /** + * Returns whether the given {@link TypeMirror} is an instance of Iterator (subtype). This is + * determined by getting the class of the TypeMirror and checking whether it is assignable from + * Iterator. + * + * @param type the TypeMirror + * @return whether type is an instance of Iterator + */ + public static boolean isIterator(TypeMirror type) { + if (type == null) return false; + Class elementRawType = TypesUtils.getClassFromType(type); + if (elementRawType == null) return false; + return Iterator.class.isAssignableFrom(elementRawType); + } + + /** + * Returns whether the given Element is a java.util.Iterator type by checking whether the raw type + * of the element is assignable from java.util.Iterator. Returns false if element is null, or has + * no valid type. + * + * @param element the element + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given element is a Java.util.Iterator type + */ + public static boolean isIterator(Element element, AnnotatedTypeFactory atf) { + if (element == null) return false; + AnnotatedTypeMirror elementTypeMirror = atf.getAnnotatedType(element).getErased(); + if (elementTypeMirror == null || elementTypeMirror.getUnderlyingType() == null) return false; + return isIterator(elementTypeMirror.getUnderlyingType()); + } + + /** + * Returns whether the given Tree is a java.util.Iterator type by checking whether the raw type of + * the element is assignable from java.util.Iterator. Returns false if tree is null, or has no + * valid type. + * + * @param tree the tree + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given Tree is a Java.util.Iterator type + */ + public static boolean isIterator(Tree tree, AnnotatedTypeFactory atf) { + if (tree == null) return false; + if (tree instanceof MethodInvocationTree) { + tree = ((MethodInvocationTree) tree).getMethodSelect(); + } + Element element = TreeUtils.elementFromTree(tree); + return isIterator(element, atf); + } + + /** + * Returns whether the given Node is a java.util.Iterator type by checking whether the raw type of + * the element is assignable from java.util.Iterator. If node is a method invocation or access, + * its return type is analyzed instead. Returns false if tree is null, or has no valid type. + * + * @param node the node + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given Node is a Java.util.Iterator type + */ + public static boolean isIterator(Node node, AnnotatedTypeFactory atf) { + if (node == null) return false; + if (node instanceof MethodInvocationNode) { + node = ((MethodInvocationNode) node).getTarget(); + } + if (node instanceof MethodAccessNode) { + return isIterator(((MethodAccessNode) node).getMethod().getReturnType()); + } + return isIterator(node.getTree(), atf); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/IOUtils.astub b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/IOUtils.astub similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/IOUtils.astub rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/IOUtils.astub diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnalysis.java similarity index 69% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnalysis.java index 1dab92a1da8f..1e4a7ecd35b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnalysis.java @@ -1,15 +1,16 @@ -package org.checkerframework.checker.resourceleak; +package org.checkerframework.checker.rlccalledmethods; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.CalledMethodsAnalysis; import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.resourceleak.SetOfTypes; /** * This variant of CFAnalysis extends the set of ignored exception types. * - * @see ResourceLeakChecker#getIgnoredExceptions() + * @see RLCCalledMethodsChecker#getIgnoredExceptions() */ -public class ResourceLeakAnalysis extends CalledMethodsAnalysis { +public class RLCCalledMethodsAnalysis extends CalledMethodsAnalysis { /** * The set of exceptions to ignore, cached from {@link @@ -23,8 +24,8 @@ public class ResourceLeakAnalysis extends CalledMethodsAnalysis { * @param checker the checker * @param factory the factory */ - protected ResourceLeakAnalysis( - ResourceLeakChecker checker, CalledMethodsAnnotatedTypeFactory factory) { + protected RLCCalledMethodsAnalysis( + RLCCalledMethodsChecker checker, CalledMethodsAnnotatedTypeFactory factory) { super(checker, factory); this.ignoredExceptions = checker.getIgnoredExceptions(); } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java similarity index 92% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 321e0d2ef6d0..9a673e623b61 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.resourceleak; +package org.checkerframework.checker.rlccalledmethods; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; @@ -28,6 +28,9 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; +import org.checkerframework.checker.resourceleak.MustCallInference; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; @@ -45,11 +48,11 @@ import org.checkerframework.javacutil.TypeSystemError; /** - * The type factory for the Resource Leak Checker. The main difference between this and the Called + * The type factory for the RLCCalledMethodsChecker. The main difference between this and the Called * Methods type factory from which it is derived is that this version's {@link * #postAnalyze(ControlFlowGraph)} method checks that must-call obligations are fulfilled. */ -public class ResourceLeakAnnotatedTypeFactory extends CalledMethodsAnnotatedTypeFactory +public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotatedTypeFactory implements CreatesMustCallForElementSupplier { /** The MustCall.value element/field. */ @@ -57,11 +60,11 @@ public class ResourceLeakAnnotatedTypeFactory extends CalledMethodsAnnotatedType TreeUtils.getMethod(MustCall.class, "value", 0, processingEnv); /** The EnsuresCalledMethods.value element/field. */ - /*package-private*/ final ExecutableElement ensuresCalledMethodsValueElement = + public final ExecutableElement ensuresCalledMethodsValueElement = TreeUtils.getMethod(EnsuresCalledMethods.class, "value", 0, processingEnv); /** The EnsuresCalledMethods.methods element/field. */ - /*package-private*/ final ExecutableElement ensuresCalledMethodsMethodsElement = + public final ExecutableElement ensuresCalledMethodsMethodsElement = TreeUtils.getMethod(EnsuresCalledMethods.class, "methods", 0, processingEnv); /** The EnsuresCalledMethods.List.value element/field. */ @@ -96,11 +99,11 @@ public class ResourceLeakAnnotatedTypeFactory extends CalledMethodsAnnotatedType private final BiMap tempVarToTree = HashBiMap.create(); /** - * Creates a new ResourceLeakAnnotatedTypeFactory. + * Creates a new RLCCalledMethodsAnnotatedTypeFactory. * * @param checker the checker associated with this type factory */ - public ResourceLeakAnnotatedTypeFactory(BaseTypeChecker checker) { + public RLCCalledMethodsAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.noResourceAliases = checker.hasOption(MustCallChecker.NO_RESOURCE_ALIASES); this.postInit(); @@ -125,7 +128,9 @@ public AnnotationMirror createCalledMethods(String... val) { @Override public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer(this, (ResourceLeakAnalysis) this.analysis); + new MustCallConsistencyAnalyzer( + ResourceLeakUtils.getResourceLeakChecker(this), + (RLCCalledMethodsAnalysis) this.analysis); mustCallConsistencyAnalyzer.analyze(cfg); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for @@ -141,8 +146,8 @@ public void postAnalyze(ControlFlowGraph cfg) { } @Override - protected ResourceLeakAnalysis createFlowAnalysis() { - return new ResourceLeakAnalysis((ResourceLeakChecker) checker, this); + protected RLCCalledMethodsAnalysis createFlowAnalysis() { + return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); } /** @@ -180,7 +185,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param tree a tree * @return true if the Must Call type is non-empty or top */ - /*package-private*/ boolean hasEmptyMustCallValue(Tree tree) { + public boolean hasEmptyMustCallValue(Tree tree) { AnnotationMirror mustCallAnnotation = getMustCallAnnotation(tree); if (mustCallAnnotation != null) { return getMustCallValues(mustCallAnnotation).isEmpty(); @@ -201,7 +206,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param element an element * @return true if the Must Call type is non-empty or top */ - /*package-private*/ boolean hasEmptyMustCallValue(Element element) { + public boolean hasEmptyMustCallValue(Element element) { AnnotationMirror mustCallAnnotation = getMustCallAnnotation(element); if (mustCallAnnotation != null) { return getMustCallValues(mustCallAnnotation).isEmpty(); @@ -219,7 +224,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { *

Do not use this method to get the MustCall values of an {@link * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, use * {@link - * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory, + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(RLCCalledMethodsAnnotatedTypeFactory, * CFStore)}. * *

Do not call {@link List#isEmpty()} on the result of this method: prefer to call {@link @@ -228,7 +233,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param element an element * @return the strings in its must-call type */ - /*package-private*/ List getMustCallValues(Element element) { + public List getMustCallValues(Element element) { MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = getTypeFactoryOfSubchecker(MustCallChecker.class); AnnotatedTypeMirror mustCallAnnotatedType = @@ -245,8 +250,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @return the strings in mustCallAnnotation's value element, or the empty list if * mustCallAnnotation is null */ - /*package-private*/ List getMustCallValues( - @Nullable AnnotationMirror mustCallAnnotation) { + public List getMustCallValues(@Nullable AnnotationMirror mustCallAnnotation) { if (mustCallAnnotation == null) { return Collections.emptyList(); } @@ -260,7 +264,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param node a node * @return the tempvar for node's expression, or null if one does not exist */ - /*package-private*/ @Nullable LocalVariableNode getTempVarForNode(Node node) { + public @Nullable LocalVariableNode getTempVarForNode(Node node) { return tempVarToTree.inverse().get(node.getTree()); } @@ -270,7 +274,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param node a node * @return true iff the given node is a temporary variable */ - /*package-private*/ boolean isTempVar(Node node) { + public boolean isTempVar(Node node) { return tempVarToTree.containsKey(node); } @@ -280,7 +284,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param node a node for a temporary variable * @return the tree for {@code node} */ - /*package-private*/ Tree getTreeForTempVar(Node node) { + public Tree getTreeForTempVar(Node node) { if (!tempVarToTree.containsKey(node)) { throw new TypeSystemError(node + " must be a temporary variable"); } @@ -293,7 +297,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param tmpVar a temporary variable * @param tree the tree of the expression the tempvar represents */ - /*package-private*/ void addTempVar(LocalVariableNode tmpVar, Tree tree) { + public void addTempVar(LocalVariableNode tmpVar, Tree tree) { if (!tempVarToTree.containsValue(tree)) { tempVarToTree.put(tmpVar, tree); } @@ -314,7 +318,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param tree a tree * @return whether the tree has declared must-call obligations */ - /*package-private*/ boolean declaredTypeHasMustCall(Tree tree) { + public boolean declaredTypeHasMustCall(Tree tree) { assert tree.getKind() == Tree.Kind.METHOD || tree.getKind() == Tree.Kind.VARIABLE || tree.getKind() == Tree.Kind.NEW_CLASS @@ -330,7 +334,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param tree a tree * @return true if the given tree has an {@link MustCallAlias} annotation */ - /*package-private*/ boolean hasMustCallAlias(Tree tree) { + public boolean hasMustCallAlias(Tree tree) { Element elt = TreeUtils.elementFromTree(tree); return hasMustCallAlias(elt); } @@ -342,7 +346,7 @@ public AnnotationMirror getMustCallAnnotation(Object obj) { * @param elt an element * @return true if the given element has an {@link MustCallAlias} annotation */ - /*package-private*/ boolean hasMustCallAlias(Element elt) { + public boolean hasMustCallAlias(Element elt) { if (noResourceAliases) { return false; } @@ -457,7 +461,7 @@ public Set getExceptionalPostconditions( // This override is a sneaky way to satisfy a few subtle design constraints: // 1. The RLC requires destructors to close the class's @Owning fields even on exception - // (see ResourceLeakVisitor.checkOwningField). + // (see RLCCalledMethodsVisitor.checkOwningField). // 2. In versions 3.39.0 and earlier, the RLC did not have the annotation // @EnsuresCalledMethodsOnException, meaning that for destructors it had to treat // a simple @EnsuresCalledMethods annotation as serving both purposes. diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java new file mode 100644 index 000000000000..027d72e0a26b --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -0,0 +1,67 @@ +package org.checkerframework.checker.rlccalledmethods; + +import java.util.Set; +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.resourceleak.SetOfTypes; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SourceChecker; + +/** + * The entry point for the RLCCalledMethodsChecker. This checker is a modifed {@link + * CalledMethodsChecker} used as a subchecker in the ResourceLeakChecker, and never independently. + * Runs the MustCallChecker as a subchecker in order to share the cfg. + */ +@StubFiles("IOUtils.astub") +public class RLCCalledMethodsChecker extends CalledMethodsChecker { + + /** Creates a RLCCalledMethodsChecker. */ + public RLCCalledMethodsChecker() {} + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new RLCCalledMethodsVisitor(this); + } + + /** + * Get the set of exceptions that should be ignored. This set comes from the {@link + * ResourceLeakChecker#IGNORED_EXCEPTIONS} option if it was provided, or {@link + * ResourceLeakChecker#DEFAULT_IGNORED_EXCEPTIONS} if not. + * + * @return the set of exceptions to ignore + */ + public SetOfTypes getIgnoredExceptions() { + return ResourceLeakUtils.getResourceLeakChecker(this).getIgnoredExceptions(); + } + + @Override + public boolean isReturnsReceiverDisabled() { + return super.isReturnsReceiverDisabled(); + } + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + + if (hasOptionNoSubcheckers(MustCallChecker.NO_CREATES_MUSTCALLFOR)) { + checkers.add(MustCallNoCreatesMustCallForChecker.class); + } else { + checkers.add(MustCallChecker.class); + } + + return checkers; + } + + // /** + // * Returns the ResourceLeakChecker. + // * + // * @return the ResourceLeakChecker + // */ + // public ResourceLeakChecker getResourceLeakChecker() { + // return (ResourceLeakChecker) getParentChecker().getParentChecker(); + // } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 8ec62d954fad..80ee6f1cb74b 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.resourceleak; +package org.checkerframework.checker.rlccalledmethods; import com.sun.source.tree.MethodInvocationTree; import java.util.List; @@ -25,23 +25,23 @@ import org.checkerframework.javacutil.TypesUtils; /** The transfer function for the resource-leak extension to the called-methods type system. */ -public class ResourceLeakTransfer extends CalledMethodsTransfer { +public class RLCCalledMethodsTransfer extends CalledMethodsTransfer { /** * Shadowed because we must dispatch to the Resource Leak Checker's version of * getTypefactoryOfSubchecker to get the correct MustCallAnnotatedTypeFactory. */ - private final ResourceLeakAnnotatedTypeFactory rlTypeFactory; + private final RLCCalledMethodsAnnotatedTypeFactory rlTypeFactory; /** * Create a new resource leak transfer function. * * @param analysis the analysis. Its type factory must be a {@link - * ResourceLeakAnnotatedTypeFactory}. + * RLCCalledMethodsAnnotatedTypeFactory}. */ - public ResourceLeakTransfer(ResourceLeakAnalysis analysis) { + public RLCCalledMethodsTransfer(RLCCalledMethodsAnalysis analysis) { super(analysis); - this.rlTypeFactory = (ResourceLeakAnnotatedTypeFactory) analysis.getTypeFactory(); + this.rlTypeFactory = (RLCCalledMethodsAnnotatedTypeFactory) analysis.getTypeFactory(); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java similarity index 96% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java index 9ccf3b5ed70b..356735fd221b 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java @@ -1,4 +1,4 @@ -package org.checkerframework.checker.resourceleak; +package org.checkerframework.checker.rlccalledmethods; import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; @@ -25,6 +25,8 @@ import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.mustcall.qual.PolyMustCall; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.JavaExpression; @@ -44,17 +46,17 @@ * Owning} fields are satisfied, and for checking that {@link CreatesMustCallFor} overrides are * valid. */ -public class ResourceLeakVisitor extends CalledMethodsVisitor { +public class RLCCalledMethodsVisitor extends CalledMethodsVisitor { /** True if errors related to static owning fields should be suppressed. */ private final boolean permitStaticOwning; /** * Because CalledMethodsVisitor doesn't have a type parameter, we need a reference to the type - * factory that has this static type to access the features that ResourceLeakAnnotatedTypeFactory - * implements but CalledMethodsAnnotatedTypeFactory does not. + * factory that has this static type to access the features that + * RLCCalledMethodsAnnotatedTypeFactory implements but CalledMethodsAnnotatedTypeFactory does not. */ - private final ResourceLeakAnnotatedTypeFactory rlTypeFactory; + private final RLCCalledMethodsAnnotatedTypeFactory rlTypeFactory; /** True if -AnoLightweightOwnership was supplied on the command line. */ private final boolean noLightweightOwnership; @@ -70,17 +72,17 @@ public class ResourceLeakVisitor extends CalledMethodsVisitor { * * @param checker the type-checker associated with this visitor */ - public ResourceLeakVisitor(BaseTypeChecker checker) { + public RLCCalledMethodsVisitor(BaseTypeChecker checker) { super(checker); - rlTypeFactory = (ResourceLeakAnnotatedTypeFactory) atypeFactory; + rlTypeFactory = (RLCCalledMethodsAnnotatedTypeFactory) atypeFactory; permitStaticOwning = checker.hasOption("permitStaticOwning"); noLightweightOwnership = checker.hasOption("noLightweightOwnership"); enableWpiForRlc = checker.hasOption(ResourceLeakChecker.ENABLE_WPI_FOR_RLC); } @Override - protected ResourceLeakAnnotatedTypeFactory createTypeFactory() { - return new ResourceLeakAnnotatedTypeFactory(checker); + protected RLCCalledMethodsAnnotatedTypeFactory createTypeFactory() { + return new RLCCalledMethodsAnnotatedTypeFactory(checker); } @Override @@ -343,16 +345,16 @@ private static String getCreatesMustCallForValue( * * @param elt an executable element * @param mcAtf a MustCallAnnotatedTypeFactory, to source the value element - * @param atypeFactory a ResourceLeakAnnotatedTypeFactory + * @param atypeFactory a RLCCalledMethodsAnnotatedTypeFactory * @return the literal strings present in the @CreatesMustCallFor annotation(s) of that element, * substituting the default "this" for empty annotations. This method returns the empty list * iff there are no @CreatesMustCallFor annotations on elt. The returned list is always * modifiable if it is non-empty. */ - /*package-private*/ static List getCreatesMustCallForValues( + public static List getCreatesMustCallForValues( ExecutableElement elt, MustCallAnnotatedTypeFactory mcAtf, - ResourceLeakAnnotatedTypeFactory atypeFactory) { + RLCCalledMethodsAnnotatedTypeFactory atypeFactory) { AnnotationMirror createsMustCallForList = atypeFactory.getDeclAnnotation(elt, CreatesMustCallFor.List.class); List result = new ArrayList<>(4); @@ -378,12 +380,12 @@ private static String getCreatesMustCallForValue( * Get all {@link EnsuresCalledMethods} annotations on an element. * * @param elt an executable element that might have {@link EnsuresCalledMethods} annotations - * @param atypeFactory a ResourceLeakAnnotatedTypeFactory + * @param atypeFactory a RLCCalledMethodsAnnotatedTypeFactory * @return a set of {@link EnsuresCalledMethods} annotations */ @Pure private static AnnotationMirrorSet getEnsuresCalledMethodsAnnotations( - ExecutableElement elt, ResourceLeakAnnotatedTypeFactory atypeFactory) { + ExecutableElement elt, RLCCalledMethodsAnnotatedTypeFactory atypeFactory) { AnnotationMirror ensuresCalledMethodsAnnos = atypeFactory.getDeclAnnotation(elt, EnsuresCalledMethods.List.class); AnnotationMirrorSet result = new AnnotationMirrorSet(); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties rename to checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties From a002dee54e0ed93f44f5142f3ca2b8c32aae9077 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 10 Mar 2025 14:34:55 +0100 Subject: [PATCH 002/374] check simplename instead of canonicalname for checkers --- .../checker/resourceleak/ResourceLeakUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 2d4fa660fa79..c4cb8ae9f62f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -114,7 +114,7 @@ public class ResourceLeakUtils { throw new IllegalArgumentException("Argument referenceChecker cannot be null"); } - String className = referenceChecker.getClass().getCanonicalName(); + String className = referenceChecker.getClass().getSimpleName(); if ("ResourceLeakChecker".equals(className)) { return (ResourceLeakChecker) referenceChecker; } else if ("RLCCalledMethodsChecker".equals(className) @@ -143,7 +143,7 @@ public class ResourceLeakUtils { throw new IllegalArgumentException("Argument referenceChecker cannot be null"); } - String className = referenceChecker.getClass().getCanonicalName(); + String className = referenceChecker.getClass().getSimpleName(); if ("RLCCalledMethodsChecker".equals(className)) { return (RLCCalledMethodsChecker) referenceChecker; } else if ("ResourceLeakChecker".equals(className)) { From 1a6d566d8d572276112f4878b23d01f212c497f9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 10 Mar 2025 14:35:20 +0100 Subject: [PATCH 003/374] set root of rlc before calling consistency analyzer --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 9a673e623b61..87ac2085a8da 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.MustCallInference; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -55,6 +56,9 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotatedTypeFactory implements CreatesMustCallForElementSupplier { + /** The rlc parent checker. */ + private ResourceLeakChecker rlc; + /** The MustCall.value element/field. */ private final ExecutableElement mustCallValueElement = TreeUtils.getMethod(MustCall.class, "value", 0, processingEnv); @@ -105,8 +109,11 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated */ public RLCCalledMethodsAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - this.noResourceAliases = checker.hasOption(MustCallChecker.NO_RESOURCE_ALIASES); - this.postInit(); + this.rlc = ResourceLeakUtils.getResourceLeakChecker(checker); + this.noResourceAliases = rlc.hasOption(MustCallChecker.NO_RESOURCE_ALIASES); + if (this.getClass() == RLCCalledMethodsAnnotatedTypeFactory.class) { + this.postInit(); + } } @Override @@ -127,10 +134,9 @@ public AnnotationMirror createCalledMethods(String... val) { @Override public void postAnalyze(ControlFlowGraph cfg) { + rlc.setRoot(root); MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer( - ResourceLeakUtils.getResourceLeakChecker(this), - (RLCCalledMethodsAnalysis) this.analysis); + new MustCallConsistencyAnalyzer(rlc, (RLCCalledMethodsAnalysis) this.analysis); mustCallConsistencyAnalyzer.analyze(cfg); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for @@ -378,7 +384,7 @@ public boolean hasCreatesMustCallFor(MethodInvocationNode node) { */ public boolean canCreateObligations() { // Precomputing this call to `hasOption` causes a NullPointerException, so leave it as is. - return !checker.hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); + return !rlc.hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); } @Override From 0e6c4077770490c8030f7a81786b6a9e6da7eb1f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 10 Mar 2025 15:05:16 +0100 Subject: [PATCH 004/374] remove uncalled methods in rlc (moved to rlccm) --- .../resourceleak/ResourceLeakChecker.java | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index 34ccf6488235..5995e23d6f19 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -16,7 +16,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; -import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.source.SupportedOptions; @@ -46,7 +45,6 @@ ResourceLeakChecker.ENABLE_WPI_FOR_RLC, ResourceLeakChecker.ENABLE_RETURNS_RECEIVER }) -@StubFiles("IOUtils.astub") public class ResourceLeakChecker extends AggregateChecker { /** Creates a ResourceLeakChecker. */ @@ -186,15 +184,14 @@ public void typeProcessingOver() { super.typeProcessingOver(); } - /** - * Disable the Returns Receiver Checker unless it has been explicitly enabled with the {@link - * #ENABLE_RETURNS_RECEIVER} option. - */ - protected boolean isReturnsReceiverDisabled() { - RLCCalledMethodsChecker rlccmc = - (RLCCalledMethodsChecker) ResourceLeakUtils.getChecker(RLCCalledMethodsChecker.class, this); - return !hasOption(ENABLE_RETURNS_RECEIVER) || rlccmc.isReturnsReceiverDisabled(); - } + // /** + // * Disable the Returns Receiver Checker unless it has been explicitly enabled with the {@link + // * #ENABLE_RETURNS_RECEIVER} option. + // */ + // protected boolean isReturnsReceiverDisabled() { + // RLCCalledMethodsChecker rlccmc = ResourceLeakUtils.getRLCCalledMethodsChecker(this); + // return !hasOption(ENABLE_RETURNS_RECEIVER) || rlccmc.isReturnsReceiverDisabled(); + // } /** * Get the set of exceptions that should be ignored. This set comes from the {@link @@ -306,12 +303,4 @@ protected SetOfTypes parseIgnoredExceptions(String ignoredExceptionsOptionValue) } return types.getDeclaredType(elem); } - - // @Override - // public NavigableSet getSuppressWarningsPrefixes() { - // NavigableSet result = super.getSuppressWarningsPrefixes(); - // result.add("builder"); - // return result; - // } - } From 1617edef006b2819e64a7acbaf566d5311dac1aa Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 10 Mar 2025 15:05:38 +0100 Subject: [PATCH 005/374] add rlc as field to rlccm --- .../RLCCalledMethodsChecker.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 027d72e0a26b..540f86d7ca3a 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -22,6 +22,9 @@ public class RLCCalledMethodsChecker extends CalledMethodsChecker { /** Creates a RLCCalledMethodsChecker. */ public RLCCalledMethodsChecker() {} + /** The parent resource leak checker */ + private ResourceLeakChecker rlc; + @Override protected BaseTypeVisitor createSourceVisitor() { return new RLCCalledMethodsVisitor(this); @@ -35,7 +38,7 @@ protected BaseTypeVisitor createSourceVisitor() { * @return the set of exceptions to ignore */ public SetOfTypes getIgnoredExceptions() { - return ResourceLeakUtils.getResourceLeakChecker(this).getIgnoredExceptions(); + return getResourceLeakChecker().getIgnoredExceptions(); } @Override @@ -56,12 +59,11 @@ protected Set> getImmediateSubcheckerClasses() { return checkers; } - // /** - // * Returns the ResourceLeakChecker. - // * - // * @return the ResourceLeakChecker - // */ - // public ResourceLeakChecker getResourceLeakChecker() { - // return (ResourceLeakChecker) getParentChecker().getParentChecker(); - // } + private ResourceLeakChecker getResourceLeakChecker() { + if (this.rlc == null) { + this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); + } + + return this.rlc; + } } From def97d4d3544746ce838c3b8ee0a31443832a1db Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 13 Mar 2025 19:15:31 +0100 Subject: [PATCH 006/374] add missing import for javadoc --- .../checker/rlccalledmethods/RLCCalledMethodsTransfer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 80ee6f1cb74b..28bd017fafec 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -9,6 +9,7 @@ import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.dataflow.analysis.TransferInput; From b78287540497a840d6d2566c53ea08f6e5ad0514 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 15 Mar 2025 12:07:54 +0100 Subject: [PATCH 007/374] replace deprecated 'builder' with 'resourceleak' in warning suppressions --- .../framework/stub/AnnotationFileElementTypes.java | 2 +- .../checkerframework/framework/stub/AnnotationFileUtil.java | 2 +- .../java/org/checkerframework/framework/util/CheckerMain.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java index 0c6659be5737..ef83b83daeff 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java @@ -300,7 +300,7 @@ public void parseAjavaFileWithTree(String ajavaPath, CompilationUnitTree root) { * @param annotationFiles list of files and directories to parse * @param fileType the file type of files to parse */ - @SuppressWarnings("builder:required.method.not.called" // `allFiles` may contain multiple + @SuppressWarnings("resourceleak:required.method.not.called" // `allFiles` may contain multiple // JarEntryAnnotationFileResource. Each of those references a zip file entry resource, which // itself references a ZipFile resource -- the same ZipFile for multiple zip file entries. // Closing any one of the zip file entries will close the ZipFile, which invalidates the diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java index 414a07c1dcca..521746202d65 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java @@ -444,7 +444,7 @@ private static boolean isJar(File f) { @SuppressWarnings({ "JdkObsolete", // JarFile.entries() "nullness:argument", // inference failed in Arrays.sort - "builder:required.method.not.called" // ownership passed to list of + "resourceleak:required.method.not.called" // ownership passed to list of // JarEntryAnnotationFileResource, where `file` appears in every element of the list }) private static void addAnnotationFilesToList( diff --git a/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java b/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java index e9161359c24a..ad66b0d68e88 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java +++ b/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java @@ -581,7 +581,8 @@ private static void outputArgumentsToFile(String outputFilename, List ar String errorMessage = null; try { - @SuppressWarnings("builder:required.method.not.called") // don't want to close System.out + @SuppressWarnings( + "resourceleak:required.method.not.called") // don't want to close System.out PrintWriter writer = (outputFilename.equals("-") ? new PrintWriter( From a604f7ad661d9b02fab780e2093382f1a83baf6f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 15 Mar 2025 12:45:36 +0100 Subject: [PATCH 008/374] add doc for resourceleakutils --- .../resourceleak/ResourceLeakUtils.java | 297 ++---------------- 1 file changed, 25 insertions(+), 272 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index c4cb8ae9f62f..affa2e602864 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -1,38 +1,14 @@ package org.checkerframework.checker.resourceleak; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; -import javax.lang.model.type.WildcardType; -import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; -import org.checkerframework.checker.mustcall.qual.InheritableMustCall; -import org.checkerframework.checker.mustcall.qual.MustCall; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.cfg.node.MethodAccessNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * Collection of static utility functions related to the various (sub-) checkers within the @@ -50,55 +26,12 @@ public class ResourceLeakUtils { MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); /** - * Returns the type factory corresponding to the desired checker class within the - * ResourceLeakChecker given a checker part of the ResourceLeakChecker. + * Given a type factory part of the resource leak ecosystem, returns the {@link + * ResourceLeakChecker} in the checker hierarchy. * - * @param targetClass the desired checker class - * @param referenceChecker the current checker - * @return the type factory of the desired class + * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from + * @return the {@link ResourceLeakChecker} in the checker hierarchy */ - public static @NonNull AnnotatedTypeFactory getTypeFactory( - Class targetClass, SourceChecker referenceChecker) { - BaseTypeChecker targetChecker = (BaseTypeChecker) getChecker(targetClass, referenceChecker); - return targetChecker.getTypeFactory(); - } - - /** - * Returns the type factory corresponding to the desired checker class within the - * ResourceLeakChecker given a type factory part of the ResourceLeakChecker. - * - * @param targetClass the desired checker class - * @param referenceAtf the current atf - * @return the type factory of the desired class - */ - public static @NonNull AnnotatedTypeFactory getTypeFactory( - Class targetClass, AnnotatedTypeFactory referenceAtf) { - if (!rlcCheckers.contains(targetClass.getCanonicalName())) { - throw new IllegalArgumentException( - "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " - + targetClass.getCanonicalName()); - } - return ((BaseTypeChecker) getChecker(targetClass, referenceAtf.getChecker())).getTypeFactory(); - } - - /** - * Returns the checker of the desired class within the ResourceLeakChecker given a type factory - * part of the ResourceLeakChecker. - * - * @param targetClass the desired checker class - * @param referenceAtf the current atf - * @return the checker of the desired class - */ - public static @NonNull SourceChecker getChecker( - Class targetClass, AnnotatedTypeFactory referenceAtf) { - if (!rlcCheckers.contains(targetClass.getCanonicalName())) { - throw new IllegalArgumentException( - "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " - + targetClass.getCanonicalName()); - } - return getChecker(targetClass, referenceAtf.getChecker()); - } - public static @NonNull ResourceLeakChecker getResourceLeakChecker( AnnotatedTypeFactory referenceAtf) { if (referenceAtf == null) { @@ -108,6 +41,13 @@ public class ResourceLeakUtils { } } + /** + * Given a checker part of the resource leak ecosystem, returns the {@link ResourceLeakChecker} in + * the checker hierarchy. + * + * @param referenceChecker the checker to retrieve the {@link ResourceLeakChecker} from + * @return the {@link ResourceLeakChecker} in the checker hierarchy + */ public static @NonNull ResourceLeakChecker getResourceLeakChecker( SourceChecker referenceChecker) { if (referenceChecker == null) { @@ -128,6 +68,13 @@ public class ResourceLeakUtils { } } + /** + * Given a type factory part of the resource leak ecosystem, returns the {@link + * RLCCalledMethodsChecker} in the checker hierarchy. + * + * @param referenceAtf the type factory to retrieve the {@link RLCCalledMethodsChecker} from + * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy + */ public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( AnnotatedTypeFactory referenceAtf) { if (referenceAtf == null) { @@ -137,6 +84,13 @@ public class ResourceLeakUtils { } } + /** + * Given a checker part of the resource leak ecosystem, returns the {@link + * RLCCalledMethodsChecker} in the checker hierarchy. + * + * @param referenceChecker the checker to retrieve the {@link RLCCalledMethodsChecker} from + * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy + */ public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( SourceChecker referenceChecker) { if (referenceChecker == null) { @@ -158,205 +112,4 @@ public class ResourceLeakUtils { + className); } } - - /** - * Returns the checker of the desired class given a checker part of the RLC. Both the targetClass - * and reference checker must be checkers from the RLC ecosystem, as defined by {@code - * this.rlcCheckers}. - * - * @param targetClass the desired checker class - * @param referenceChecker the current checker - * @return the checker of the desired class - * @throws IllegalArgumentException when either of the arguments is not one of the RLC checkers - */ - public static @NonNull SourceChecker getChecker( - Class targetClass, SourceChecker referenceChecker) { - if (!rlcCheckers.contains(targetClass.getCanonicalName())) { - throw new IllegalArgumentException( - "Argument targetClass to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " - + targetClass.getCanonicalName()); - } - Class refClass = referenceChecker.getClass(); - if (refClass == targetClass) { - // base case -- we found the desired checker - return referenceChecker; - } else if (refClass == MustCallChecker.class) { - return getChecker(targetClass, referenceChecker.getParentChecker()); - } else if (refClass == ResourceLeakChecker.class) { - return getChecker(targetClass, referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); - } else if (refClass == RLCCalledMethodsChecker.class) { - if (targetClass == MustCallChecker.class) { - MustCallChecker mcc = referenceChecker.getSubchecker(MustCallChecker.class); - return mcc != null - ? mcc - : referenceChecker.getSubchecker(MustCallNoCreatesMustCallForChecker.class); - } else { - return getChecker(targetClass, referenceChecker.getParentChecker()); - } - } else { - throw new IllegalArgumentException( - "Argument referenceChecker to ResourceLeakUtils#getChecker(targetClass, referenceChecker) expected to be an RLC checker but is " - + refClass.getCanonicalName()); - } - } - - /** - * Returns the list of mustcall obligations for the given {@code TypeMirror} upper bound (either - * the type variable itself if it is concrete or the upper bound if its a wildcard or generic). - * - *

If the type variable has no upper bound, for instance if it is a wildcard with no extends - * clause the method returns null - * - * @param type the {@code TypeMirror} - * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type - * @return the list of mustcall obligations for the upper bound of {@code type} or null if the - * upper bound is null. - */ - public static @Nullable List getMcValues( - TypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { - if (type instanceof TypeVariable) { - // a generic - replace with upper bound and return null if it has no upper bound - type = ((TypeVariable) type).getUpperBound(); - if (type == null) { - return null; - } - } else if (type instanceof WildcardType) { - // a wildcard - replace with upper bound and return null if it has no upper bound - type = ((WildcardType) type).getExtendsBound(); - if (type == null) { - return null; - } - } - TypeElement typeElement = TypesUtils.getTypeElement(type); - AnnotationMirror imcAnnotation = - mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); - AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); - Set mcValues = new HashSet<>(); - if (mcAnnotation != null) { - mcValues.addAll( - AnnotationUtils.getElementValueArray( - mcAnnotation, mcAtf.getMustCallValueElement(), String.class)); - } - if (imcAnnotation != null) { - mcValues.addAll( - AnnotationUtils.getElementValueArray( - imcAnnotation, mcAtf.getInheritableMustCallValueElement(), String.class)); - } - return new ArrayList<>(mcValues); - } - - /** - * Returns whether the given {@link TypeMirror} is an instance of a collection (subclass). This is - * determined by getting the class of the TypeMirror and checking whether it is assignable from - * Collection. - * - * @param type the TypeMirror - * @return whether type is an instance of a collection (subclass) - */ - public static boolean isCollection(TypeMirror type) { - if (type == null) return false; - Class elementRawType = TypesUtils.getClassFromType(type); - if (elementRawType == null) return false; - return Collection.class.isAssignableFrom(elementRawType); - } - - /** - * Returns whether the given Element is a java.util.Collection type by checking whether the raw - * type of the element is assignable from java.util.Collection. Returns false if element is null, - * or has no valid type. - * - * @param element the element - * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given element is a Java.util.Collection type - */ - public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { - if (element == null) return false; - AnnotatedTypeMirror elementTypeMirror = atf.getAnnotatedType(element).getErased(); - if (elementTypeMirror == null || elementTypeMirror.getUnderlyingType() == null) return false; - return isCollection(elementTypeMirror.getUnderlyingType()); - } - - /** - * Returns whether the given Tree is a java.util.Collection type by checking whether the raw type - * of the element is assignable from java.util.Collection. Returns false if tree is null, or has - * no valid type. - * - * @param tree the tree - * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given Tree is a Java.util.Collection type - */ - public static boolean isCollection(Tree tree, AnnotatedTypeFactory atf) { - if (tree == null) return false; - Element element = TreeUtils.elementFromTree(tree); - return isCollection(element, atf); - } - - /** - * Returns whether the given {@link TypeMirror} is an instance of Iterator (subtype). This is - * determined by getting the class of the TypeMirror and checking whether it is assignable from - * Iterator. - * - * @param type the TypeMirror - * @return whether type is an instance of Iterator - */ - public static boolean isIterator(TypeMirror type) { - if (type == null) return false; - Class elementRawType = TypesUtils.getClassFromType(type); - if (elementRawType == null) return false; - return Iterator.class.isAssignableFrom(elementRawType); - } - - /** - * Returns whether the given Element is a java.util.Iterator type by checking whether the raw type - * of the element is assignable from java.util.Iterator. Returns false if element is null, or has - * no valid type. - * - * @param element the element - * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given element is a Java.util.Iterator type - */ - public static boolean isIterator(Element element, AnnotatedTypeFactory atf) { - if (element == null) return false; - AnnotatedTypeMirror elementTypeMirror = atf.getAnnotatedType(element).getErased(); - if (elementTypeMirror == null || elementTypeMirror.getUnderlyingType() == null) return false; - return isIterator(elementTypeMirror.getUnderlyingType()); - } - - /** - * Returns whether the given Tree is a java.util.Iterator type by checking whether the raw type of - * the element is assignable from java.util.Iterator. Returns false if tree is null, or has no - * valid type. - * - * @param tree the tree - * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given Tree is a Java.util.Iterator type - */ - public static boolean isIterator(Tree tree, AnnotatedTypeFactory atf) { - if (tree == null) return false; - if (tree instanceof MethodInvocationTree) { - tree = ((MethodInvocationTree) tree).getMethodSelect(); - } - Element element = TreeUtils.elementFromTree(tree); - return isIterator(element, atf); - } - - /** - * Returns whether the given Node is a java.util.Iterator type by checking whether the raw type of - * the element is assignable from java.util.Iterator. If node is a method invocation or access, - * its return type is analyzed instead. Returns false if tree is null, or has no valid type. - * - * @param node the node - * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given Node is a Java.util.Iterator type - */ - public static boolean isIterator(Node node, AnnotatedTypeFactory atf) { - if (node == null) return false; - if (node instanceof MethodInvocationNode) { - node = ((MethodInvocationNode) node).getTarget(); - } - if (node instanceof MethodAccessNode) { - return isIterator(((MethodAccessNode) node).getMethod().getReturnType()); - } - return isIterator(node.getTree(), atf); - } } From b00c63a0aabad72964926ac5ac4c04904cab4227 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 15 Mar 2025 13:11:53 +0100 Subject: [PATCH 009/374] add doc for rlccalledmethods checker --- .../checker/rlccalledmethods/RLCCalledMethodsChecker.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 540f86d7ca3a..91535a1fa6be 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -59,6 +59,12 @@ protected Set> getImmediateSubcheckerClasses() { return checkers; } + /** + * Finds the {@link ResourceLeakChecker} in the checker hierarchy, caches it in a field, and + * returns it. + * + * @return the {@link ResourceLeakChecker} in the checker hierarchy + */ private ResourceLeakChecker getResourceLeakChecker() { if (this.rlc == null) { this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); From b2442b973bf3b0d5b4a3c06480e224ce08b8ec16 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 15 Mar 2025 13:39:35 +0100 Subject: [PATCH 010/374] add doc for resourceleakutils constructor --- .../checker/resourceleak/ResourceLeakUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index affa2e602864..1957bc8c7c6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -16,6 +16,9 @@ */ public class ResourceLeakUtils { + /** Shouldn't be instantiated, pure utility class. */ + public ResourceLeakUtils() {} + /** List of checker names associated with the Resource Leak Checker. */ public static List rlcCheckers = new ArrayList<>( From f30e0df2222a6fe4787fb96460e7a6bf0e613a7c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 15 Mar 2025 14:56:13 +0100 Subject: [PATCH 011/374] get ignored exception types from rlccm type factory instead of analysis --- .../resourceleak/MustCallConsistencyAnalyzer.java | 2 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 83c3b945cbd7..28153cad5e23 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1860,7 +1860,7 @@ private boolean shouldTrackReturnType(MethodInvocationNode node) { Map> exceptionalSuccessors = excBlock.getExceptionalSuccessors(); for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { TypeMirror exceptionType = entry.getKey(); - if (!analysis.isIgnoredExceptionType(exceptionType)) { + if (!cmAtf.isIgnoredExceptionType(exceptionType)) { for (Block exSucc : entry.getValue()) { result.add(IPair.of(exSucc, exceptionType)); } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 87ac2085a8da..695e08d26e19 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -12,6 +12,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; import org.checkerframework.checker.calledmethods.EnsuresCalledMethodOnExceptionContract; import org.checkerframework.checker.calledmethods.qual.CalledMethods; @@ -528,4 +529,17 @@ private boolean isMustCallMethod(ExecutableElement elt) { String methodName = elt.getSimpleName().toString(); return mcValues.contains(methodName); } + + /** + * Returns true if the checker should ignore exceptional control flow due to the given exception + * type. + * + * @param exceptionType exception type + * @return {@code true} if {@code exceptionType} is a member of {@link + * RLCCalledMethodsAnalysis#ignoredExceptionTypes}, {@code false} otherwise + */ + @Override + public boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return ((RLCCalledMethodsAnalysis) analysis).isIgnoredExceptionType(exceptionType); + } } From 6ffe053887c526b454d765d8dd1c9d9eb2d49669 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 10:40:28 +0100 Subject: [PATCH 012/374] give rlccm type factory ability to get transfer input from flowresult --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 19 +++++++++++++++++++ .../dataflow/analysis/AnalysisResult.java | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 695e08d26e19..fd875862b6b8 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -33,9 +33,13 @@ import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.common.accumulation.AccumulationStore; +import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -542,4 +546,19 @@ private boolean isMustCallMethod(ExecutableElement elt) { public boolean isIgnoredExceptionType(TypeMirror exceptionType) { return ((RLCCalledMethodsAnalysis) analysis).isIgnoredExceptionType(exceptionType); } + + /** + * Fetches the transfer input for the given block, either from the flowResult, if the analysis is + * still running, or else from the analysis itself. + * + * @param block a block + * @return the appropriate TransferInput from the results of running dataflow + */ + public TransferInput getInput(Block block) { + if (!analysis.isRunning()) { + return flowResult.getInput(block); + } else { + return analysis.getInput(block); + } + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java index a583cc751478..bb01637a33cb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java @@ -337,6 +337,16 @@ public S getStoreBefore(Block block) { } } + /** + * Returns the TransferInput for the given block. + * + * @param block a CFG Block + * @return the TransferInput for the given block. + */ + public @Nullable TransferInput getInput(Block block) { + return inputs.get(block); + } + /** * Returns the regular store immediately after a given block. * From 867d50488ec7eff0abfa183547552837b9868b1d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 10:44:13 +0100 Subject: [PATCH 013/374] get cm stores in consistency analyzer from rlccm type factory instead of rlccm analysis --- .../resourceleak/MustCallConsistencyAnalyzer.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 28153cad5e23..4f89ecc64813 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -177,9 +177,6 @@ public class MustCallConsistencyAnalyzer { /** The Resource Leak Checker, used to issue errors. */ private final ResourceLeakChecker checker; - /** The analysis from the Resource Leak Checker, used to get input stores based on CFG blocks. */ - private final RLCCalledMethodsAnalysis analysis; - /** True if -AnoLightweightOwnership was passed on the command line. */ private final boolean noLightweightOwnership; @@ -558,7 +555,6 @@ public String stringForErrorMessage() { public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, RLCCalledMethodsAnalysis analysis) { this.cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) analysis.getTypeFactory(); this.checker = rlc; - this.analysis = analysis; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); this.noLightweightOwnership = checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP); @@ -1992,7 +1988,7 @@ private void propagateObligationsToSuccessorBlock( + " with exception type " + exceptionType; // Computed outside the Obligation loop for efficiency. - AccumulationStore regularStoreOfSuccessor = analysis.getInput(successor).getRegularStore(); + AccumulationStore regularStoreOfSuccessor = cmAtf.getInput(successor).getRegularStore(); for (Obligation obligation : obligations) { // This boolean is true if there is no evidence that the Obligation does not go out // of scope - that is, if there is definitely a resource alias that is in scope in @@ -2159,14 +2155,14 @@ private AccumulationStore getStoreForEdgeFromEmptyBlock(Block currentBlock, Bloc case CONDITIONAL_BLOCK: ConditionalBlock condBlock = (ConditionalBlock) currentBlock; if (condBlock.getThenSuccessor().equals(successor)) { - return analysis.getInput(currentBlock).getThenStore(); + return cmAtf.getInput(currentBlock).getThenStore(); } else if (condBlock.getElseSuccessor().equals(successor)) { - return analysis.getInput(currentBlock).getElseStore(); + return cmAtf.getInput(currentBlock).getElseStore(); } else { throw new BugInCF("successor not found"); } case SPECIAL_BLOCK: - return analysis.getInput(successor).getRegularStore(); + return cmAtf.getInput(successor).getRegularStore(); default: throw new BugInCF("unexpected block type " + currentBlock.getType()); } From 321cdd2ffc23c936150f6315c794104a114dcd28 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 10:50:34 +0100 Subject: [PATCH 014/374] remove rlccm analysis as formal parameter to consistency analyer --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 8 ++++---- .../RLCCalledMethodsAnnotatedTypeFactory.java | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4f89ecc64813..73941ac44a6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -549,11 +549,11 @@ public String stringForErrorMessage() { * #analyze(ControlFlowGraph)}. * * @param rlc the resource leak checker - * @param analysis the analysis from the type factory. Usually this would have protected access, - * so this constructor cannot get it directly. */ - public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, RLCCalledMethodsAnalysis analysis) { - this.cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) analysis.getTypeFactory(); + public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc) { + this.cmAtf = + (RLCCalledMethodsAnnotatedTypeFactory) + ResourceLeakUtils.getRLCCalledMethodsChecker(rlc).getTypeFactory(); this.checker = rlc; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index fd875862b6b8..79b1260bfd1b 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -140,8 +140,7 @@ public AnnotationMirror createCalledMethods(String... val) { @Override public void postAnalyze(ControlFlowGraph cfg) { rlc.setRoot(root); - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer(rlc, (RLCCalledMethodsAnalysis) this.analysis); + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); mustCallConsistencyAnalyzer.analyze(cfg); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for From 02650418018b0842efea4a7eaaaa64a71ea367af Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 11:30:35 +0100 Subject: [PATCH 015/374] change suppression prefix from resourceleak back to builder to make pr smaller --- .../framework/stub/AnnotationFileElementTypes.java | 2 +- .../checkerframework/framework/stub/AnnotationFileUtil.java | 2 +- .../java/org/checkerframework/framework/util/CheckerMain.java | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java index ef83b83daeff..0c6659be5737 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java @@ -300,7 +300,7 @@ public void parseAjavaFileWithTree(String ajavaPath, CompilationUnitTree root) { * @param annotationFiles list of files and directories to parse * @param fileType the file type of files to parse */ - @SuppressWarnings("resourceleak:required.method.not.called" // `allFiles` may contain multiple + @SuppressWarnings("builder:required.method.not.called" // `allFiles` may contain multiple // JarEntryAnnotationFileResource. Each of those references a zip file entry resource, which // itself references a ZipFile resource -- the same ZipFile for multiple zip file entries. // Closing any one of the zip file entries will close the ZipFile, which invalidates the diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java index 521746202d65..414a07c1dcca 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java @@ -444,7 +444,7 @@ private static boolean isJar(File f) { @SuppressWarnings({ "JdkObsolete", // JarFile.entries() "nullness:argument", // inference failed in Arrays.sort - "resourceleak:required.method.not.called" // ownership passed to list of + "builder:required.method.not.called" // ownership passed to list of // JarEntryAnnotationFileResource, where `file` appears in every element of the list }) private static void addAnnotationFilesToList( diff --git a/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java b/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java index ad66b0d68e88..e9161359c24a 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java +++ b/framework/src/main/java/org/checkerframework/framework/util/CheckerMain.java @@ -581,8 +581,7 @@ private static void outputArgumentsToFile(String outputFilename, List ar String errorMessage = null; try { - @SuppressWarnings( - "resourceleak:required.method.not.called") // don't want to close System.out + @SuppressWarnings("builder:required.method.not.called") // don't want to close System.out PrintWriter writer = (outputFilename.equals("-") ? new PrintWriter( From 694ca90509de92372f0b9bb472766e625f59fc87 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 11:32:16 +0100 Subject: [PATCH 016/374] add builder as warning suppresion prefix for rlc --- .../checker/resourceleak/ResourceLeakChecker.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index 5995e23d6f19..fb6b23387738 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.NavigableSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -303,4 +304,11 @@ protected SetOfTypes parseIgnoredExceptions(String ignoredExceptionsOptionValue) } return types.getDeclaredType(elem); } + + @Override + public NavigableSet getSuppressWarningsPrefixes() { + NavigableSet result = super.getSuppressWarningsPrefixes(); + result.add("builder"); + return result; + } } From e83fc6b718020b65762bc39e3b50cc42bdbae96a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 15:30:56 +0100 Subject: [PATCH 017/374] add collection ownership qualifiers --- .../qual/NotOwningCollection.java | 19 ++++++++++++ .../qual/OwningCollection.java | 24 +++++++++++++++ .../qual/OwningCollectionBottom.java | 20 +++++++++++++ .../OwningCollectionWithoutObligation.java | 30 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java new file mode 100644 index 000000000000..e0a2a82c49c8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.collectionownership.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The top qualifier in the Collection Ownership type hierarchy. + * + *

An expression of type {@code NotOwningCollection} is a resource collection/array, which may + * not own the underlying collection/array, and thus cannot add or remove elements from it. + * + *

This annotation can be enforced by running the Resource Leak Checker. It enforces that the + * expression is not used to add to or remove elements from the underlying collection/array. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface NotOwningCollection {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java new file mode 100644 index 000000000000..cb02b5f786e1 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.collectionownership.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * An expression of type {@code @OwningCollection} is a resource collection/array, which definitely + * owns the underlying collection/array. It can add or remove elements from the collection/array. + * + *

This annotation can be enforced by running the Resource Leak Checker. The dataflow analysis + * running after the type checker tracks at least one owner per underlying resource + * collection/array. In particular, if an expression has type {@code @OwningCollection}, it is also + * tracked as an owner by the dataflow analysis, which checks that it (or one of its aliases) calls + * the methods in the {@code @MustCall} type of its elements on all of its elements before leaving + * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an + * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({NotOwningCollection.class}) +public @interface OwningCollection {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java new file mode 100644 index 000000000000..c3aa62f54738 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.collectionownership.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * The bottom qualifier of the Collection Ownership type hierarchy, and the default qualifier. + * + *

An expression of type {@code @OwningCollectionBottom} is either not a collection/array, or a + * collection/array, whose elements have empty {@code @MustCall} type. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({OwningCollectionWithoutObligation.class}) +@DefaultQualifierInHierarchy +public @interface OwningCollectionBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java new file mode 100644 index 000000000000..8d57f9130de6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java @@ -0,0 +1,30 @@ +package org.checkerframework.checker.collectionownership.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; + +/** + * An expression of type {@code @OwningCollectionWithoutObligation} is a resource collection/arary, + * which definitely owns the underlying collection/array and has definitely called all of the + * methods in the {@code @MustCall} type of its elements on all of its elements. + * + *

This annotation exists such that for a destructor method {@code d} of a class {@code C} with + * an {@code @OwningCollection} field {@code f}, the post-condition of said destructor method can be + * of the form {@code @EnsuresQualifier(expression = "this.f", qualifier = + * OwningCollectionWithoutObligation.class)}. + * + *

This annotation can be enforced by running the Resource Leak Checker. The dataflow analysis + * running after the type checker tracks at least one owner per underlying resource + * collection/array. In particular, if an expression has type {@code @OwningCollection}, it is also + * tracked as an owner by the dataflow analysis, which checks that it (or one of its aliases) calls + * the methods in the {@code @MustCall} type of its elements on all of its elements before leaving + * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an + * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({OwningCollection.class}) +public @interface OwningCollectionWithoutObligation {} From 3f822a81e4e39f7c1ef731b14b572a1737774576 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 23:36:44 +0100 Subject: [PATCH 018/374] add collection ownership checker --- .../CollectionOwnershipChecker.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java new file mode 100644 index 000000000000..19360b3323b7 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -0,0 +1,10 @@ +package org.checkerframework.checker.collectionownership; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** + * This typechecker tracks at most one owning variable per resource collection using an ownership + * type system. The resource leak checker verifies that the determined owner fulfills the calling + * obligations of its elements. + */ +public class CollectionOwnershipChecker extends BaseTypeChecker {} From 31f0e6ca8fe3e885e564c97ed27289046f7b8735 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 16 Mar 2025 23:37:09 +0100 Subject: [PATCH 019/374] add collection ownership type factory --- ...llectionOwnershipAnnotatedTypeFactory.java | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java new file mode 100644 index 000000000000..8cc5be8c1fe0 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -0,0 +1,177 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.CompilationUnitTree; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; +import org.checkerframework.checker.collectionownership.qual.OwningCollection; +import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; +import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.NoElementQualifierHierarchy; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationBuilder; + +/** The annotated type factory for the Collection Ownership Checker. */ +public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The {@code @}{@link NotOwningCollection} annotation. */ + public final AnnotationMirror NOTOWNINGCOLLECTION; + + /** The {@code @}{@link OwningCollection} annotation. */ + public final AnnotationMirror OWNINGCOLLECTION; + + /** The {@code @}{@link OwningCollectionWithoutObligation} annotation. */ + public final AnnotationMirror OWNINGCOLLECTIONWITHOUTOBLIGATION; + + /** + * The {@code @}{@link OwningCollectionBottom}{@code ()} annotation. It is the default in + * unannotated code. + */ + public final AnnotationMirror BOTTOM; + + /** + * Creates a CollectionOwnershipAnnotatedTypeFactory. + * + * @param checker the checker associated with this type factory + */ + public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + NOTOWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, NotOwningCollection.class); + OWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, OwningCollection.class); + OWNINGCOLLECTIONWITHOUTOBLIGATION = + AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); + BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); + this.postInit(); + } + + @Override + public void setRoot(@Nullable CompilationUnitTree newRoot) { + super.setRoot(newRoot); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + NotOwningCollection.class, + OwningCollection.class, + OwningCollectionWithoutObligation.class, + OwningCollectionBottom.class)); + } + + // @Override + // protected TreeAnnotator createTreeAnnotator() { + // return new ListTreeAnnotator(super.createTreeAnnotator(), new MustCallTreeAnnotator(this)); + // } + + // @Override + // protected TypeAnnotator createTypeAnnotator() { + // return new ListTypeAnnotator(super.createTypeAnnotator(), new MustCallTypeAnnotator(this)); + // } + + // @Override + // protected QualifierPolymorphism createQualifierPolymorphism() { + // return new MustCallQualifierPolymorphism(processingEnv, this); + // } + + @Override + protected QualifierHierarchy createQualifierHierarchy() { + return new CollectionOwnershipQualifierHierarchy( + this.getSupportedTypeQualifiers(), elements, this); + } + + /** + * Fetches the store from the results of dataflow for {@code first}. If {@code afterFirstStore} is + * true, then the store after {@code first} is returned; if {@code afterFirstStore} is false, the + * store before {@code succ} is returned. + * + * @param afterFirstStore whether to use the store after the first block or the store before its + * successor, succ + * @param first a block + * @param succ first's successor + * @return the appropriate CFStore, populated with MustCall annotations, from the results of + * running dataflow + */ + public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ) { + return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); + } + + // /** + // * The TreeAnnotator for the MustCall type system. + // * + // *

This tree annotator treats non-owning method parameters as bottom, regardless of their + // * declared type, when they appear in the body of the method. Doing so is safe because being + // * non-owning means, by definition, that their must-call obligations are only relevant in the + // * callee. (This behavior is disabled if the {@code -AnoLightweightOwnership} option is + // passed + // to + // * the checker.) + // * + // *

The tree annotator also changes the type of resource variables to remove "close" from + // their + // * must-call types, because the try-with-resources statement guarantees that close() is + // called + // on + // * all such variables. + // */ + // private class MustCallTreeAnnotator extends TreeAnnotator { + // /** + // * Create a MustCallTreeAnnotator. + // * + // * @param mustCallAnnotatedTypeFactory the type factory + // */ + // public MustCallTreeAnnotator(MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory) { + // super(mustCallAnnotatedTypeFactory); + // } + + // @Override + // public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror type) { + // Element elt = TreeUtils.elementFromUse(tree); + // // The following changes are not desired for RLC _inference_ in unannotated programs, + // // where a goal is to infer and add @Owning annotations to formal parameters. + // // Therefore, if WPI is enabled, they should not be executed. + // if (getWholeProgramInference() == null + // && elt.getKind() == ElementKind.PARAMETER + // && (noLightweightOwnership || getDeclAnnotation(elt, Owning.class) == null)) { + // if (!type.hasPrimaryAnnotation(POLY)) { + // // Parameters that are not annotated with @Owning should be treated as bottom + // // (to suppress warnings about them). An exception is polymorphic parameters, + // // which might be @MustCallAlias (and so wouldn't be annotated with @Owning): + // // these are not modified, to support verification of @MustCallAlias + // // annotations. + // type.replaceAnnotation(BOTTOM); + // } + // } + // return super.visitIdentifier(tree, type); + // } + // } + + /** CollectionOwnership qualifier hierarchy. */ + private static class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { + + /** + * Creates a NoElementQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + * @param atypeFactory the associated type factory + */ + public CollectionOwnershipQualifierHierarchy( + Collection> qualifierClasses, + Elements elements, + GenericAnnotatedTypeFactory atypeFactory) { + super(qualifierClasses, elements, atypeFactory); + } + } +} From 59ca7ca1758a76582d239a575bbcf82262590f71 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 09:26:44 +0100 Subject: [PATCH 020/374] draft for CollectionOwnership visitor and transfer function --- .../CollectionOwnershipTransfer.java | 140 ++++++++++ .../CollectionOwnershipVisitor.java | 244 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java new file mode 100644 index 000000000000..240955ea0d18 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -0,0 +1,140 @@ +package org.checkerframework.checker.collectionownership; + +import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TypesUtils; + +/** + * Transfer function for the collection ownership type system. Its primary purpose is to create + * temporary variables for expressions (which allow those expressions to have refined information in + * the store, which the consistency checker can use). + */ +public class CollectionOwnershipTransfer extends CFTransfer { + + /** The type factory. */ + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + /** + * Create a CollectionOwnershipTransfer. + * + * @param analysis the analysis + */ + public CollectionOwnershipTransfer(CFAnalysis analysis) { + super(analysis); + atypeFactory = (CollectionOwnershipTypeFactory) analysis.getTypeFactory(); + } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = super.visitMethodInvocation(n, in); + + updateStoreWithTempVar(result, n); + // if (!noCreatesMustCallFor) { + // List targetExprs = + // CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( + // n, atypeFactory, atypeFactory); + // for (JavaExpression targetExpr : targetExprs) { + // AnnotationMirror defaultType = + // atypeFactory + // .getAnnotatedType(TypesUtils.getTypeElement(targetExpr.getType())) + // .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + + // if (result.containsTwoStores()) { + // CFStore thenStore = result.getThenStore(); + // lubWithStoreValue(thenStore, targetExpr, defaultType); + + // CFStore elseStore = result.getElseStore(); + // lubWithStoreValue(elseStore, targetExpr, defaultType); + // } else { + // CFStore store = result.getRegularStore(); + // lubWithStoreValue(store, targetExpr, defaultType); + // } + // } + // } + return result; + } + + // /** + // * Computes the LUB of the current value in the store for expr, if it exists, and defaultType. + // * Inserts that LUB into the store as the new value for expr. + // * + // * @param store a CFStore + // * @param expr an expression that might be in the store + // * @param defaultType the default type of the expression's static type + // */ + // private void lubWithStoreValue(CFStore store, JavaExpression expr, AnnotationMirror + // defaultType) { + // CFValue value = store.getValue(expr); + // CFValue defaultTypeAsCFValue = + // analysis.createSingleAnnotationValue(defaultType, expr.getType()); + // CFValue newValue = defaultTypeAsCFValue.leastUpperBound(value); + // store.replaceValue(expr, newValue); + // } + + @Override + public TransferResult visitObjectCreation( + ObjectCreationNode node, TransferInput input) { + TransferResult result = super.visitObjectCreation(node, input); + updateStoreWithTempVar(result, node); + return result; + } + + @Override + public TransferResult visitSwitchExpressionNode( + SwitchExpressionNode node, TransferInput input) { + TransferResult result = super.visitSwitchExpressionNode(node, input); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + atypeFactory.tempVars.put(node.getTree(), node.getSwitchExpressionVar()); + } + return result; + } + + /** + * This method either creates or looks up the temp var t for node, and then updates the store to + * give t the same type as {@code node}. + * + * @param node the node to be assigned to a temporary variable + * @param result the transfer result containing the store to be modified + */ + public void updateStoreWithTempVar(TransferResult result, Node node) { + // Must-call obligations on primitives are not supported. + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + LocalVariableNode temp = getTempVar(node); + if (temp != null) { + JavaExpression localExp = JavaExpression.fromNode(temp); + AnnotationMirror anm = + atypeFactory + .getAnnotatedType(node.getTree()) + .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + insertIntoStores(result, localExp, anm == null ? atypeFactory.TOP : anm); + } + } + } + + /** + * Returns the temporary variable associated with node if it exists, or else null. + * + * @param node a node, which must be an expression (not a statement) + * @return a temporary variable node representing {@code node} that can be placed into a store or + * null if it doesn't exist + */ + private @Nullable LocalVariableNode getTempVar(Node node) { + // TODO sck: might have to query the mustcallAtf instead + return atypeFactory.tempVars.get(node.getTree()); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java new file mode 100644 index 000000000000..826447f2d569 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -0,0 +1,244 @@ +package org.checkerframework.checker.collectionownership; + +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; + +/** + * The visitor for the Collection Ownership Checker. This visitor is similar to BaseTypeVisitor, but + * overrides methods that don't work well with the ownership type hierarchy because it doesn't use + * the top type as the default type. + */ +public class CollectionOwnershipVisitor + extends BaseTypeVisitor { + + /** + * Creates a new CollectionOwnershipVisitor. + * + * @param checker the type-checker associated with this visitor + */ + public CollectionOwnershipVisitor(BaseTypeChecker checker) { + super(checker); + } + + // TODO maybe check contravariance for parameters and covariance for return types here + // (and invariance for fields) + // @Override + // protected boolean validateType(Tree tree, AnnotatedTypeMirror type) { + // if (TreeUtils.isClassTree(tree)) { + // TypeElement classEle = TreeUtils.elementFromDeclaration((ClassTree) tree); + // // If no @InheritableMustCall annotation is written here, `getDeclAnnotation()` gets one + // // from stub files and supertypes. + // AnnotationMirror anyInheritableMustCall = + // atypeFactory.getDeclAnnotation(classEle, InheritableMustCall.class); + // // An @InheritableMustCall annotation that is directly present. + // AnnotationMirror directInheritableMustCall = + // AnnotationUtils.getAnnotationByClass( + // classEle.getAnnotationMirrors(), InheritableMustCall.class); + // if (anyInheritableMustCall == null) { + // if (!ElementUtils.isFinal(classEle)) { + // // There is no @InheritableMustCall annotation on this or any superclass and + // // this is a non-final class. + // // If an explicit @MustCall annotation is present, issue a warning suggesting + // // that @InheritableMustCall is probably what the programmer means, for + // // usability. + // if (atypeFactory.getDeclAnnotation(classEle, MustCall.class) != null) { + // checker.reportWarning( + // tree, "mustcall.not.inheritable", ElementUtils.getQualifiedName(classEle)); + // } + // } + // } else { + // // There is an @InheritableMustCall annotation on this, on a superclass, or in an + // // annotation file. + // // There are two possible problems: + // // 1. There is an inconsistent @MustCall on this. + // // 2. There is an explicit @InheritableMustCall here, and it is inconsistent with + // // an @InheritableMustCall annotation on a supertype. + + // // Check for problem 1. + // AnnotationMirror explicitMustCall = + // atypeFactory.fromElement(classEle).getPrimaryAnnotation(); + // if (explicitMustCall != null) { + // // There is a @MustCall annotation here. + + // List inheritableMustCallVal = + // AnnotationUtils.getElementValueArray( + // anyInheritableMustCall, + // atypeFactory.inheritableMustCallValueElement, + // String.class, + // emptyStringList); + // AnnotationMirror inheritedMCAnno = atypeFactory.createMustCall(inheritableMustCallVal); + + // // Issue an error if there is an inconsistent, user-written @MustCall annotation + // // here. + // AnnotationMirror effectiveMCAnno = type.getPrimaryAnnotation(); + // TypeMirror tm = type.getUnderlyingType(); + // if (effectiveMCAnno != null + // && !qualHierarchy.isSubtypeShallow(inheritedMCAnno, effectiveMCAnno, tm)) { + + // checker.reportError( + // tree, + // "inconsistent.mustcall.subtype", + // ElementUtils.getQualifiedName(classEle), + // effectiveMCAnno, + // anyInheritableMustCall); + // return false; + // } + // } + + // // Check for problem 2. + // if (directInheritableMustCall != null) { + + // // `inheritedImcs` is inherited @InheritableMustCall annotations. + // List inheritedImcs = new ArrayList<>(); + // for (TypeElement elt : ElementUtils.getDirectSuperTypeElements(classEle, elements)) { + // AnnotationMirror imc = atypeFactory.getDeclAnnotation(elt, + // InheritableMustCall.class); + // if (imc != null) { + // inheritedImcs.add(imc); + // } + // } + // if (!inheritedImcs.isEmpty()) { + // // There is an inherited @InheritableMustCall annotation, in addition to the + // // one written explicitly here. + // List inheritedMustCallVal = new ArrayList<>(); + // for (AnnotationMirror inheritedImc : inheritedImcs) { + // inheritedMustCallVal.addAll( + // AnnotationUtils.getElementValueArray( + // inheritedImc, atypeFactory.inheritableMustCallValueElement, String.class)); + // } + // AnnotationMirror inheritedMCAnno = atypeFactory.createMustCall(inheritedMustCallVal); + + // AnnotationMirror effectiveMCAnno = type.getPrimaryAnnotation(); + + // TypeMirror tm = type.getUnderlyingType(); + + // if (!qualHierarchy.isSubtypeShallow(inheritedMCAnno, effectiveMCAnno, tm)) { + + // checker.reportError( + // tree, + // "inconsistent.mustcall.subtype", + // ElementUtils.getQualifiedName(classEle), + // effectiveMCAnno, + // inheritedMCAnno); + // return false; + // } + // } + // } + // } + // } + // return super.validateType(tree, type); + // } + + // TODO maybe return false if user annotation is @OwningCollectionWithoutObligation + // @Override + // public boolean isValidUse( + // AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { + // // MustCallAlias annotations are always permitted on type uses, despite not technically + // // being a part of the type hierarchy. It's necessary to get the annotation from the element + // // because MustCallAlias is aliased to PolyMustCall, which is what useType would contain. + // // Note that isValidUse does not need to consider component types, on which it should be + // // called separately. + // Element elt = TreeUtils.elementFromTree(tree); + // if (elt != null) { + // if (AnnotationUtils.containsSameByClass(elt.getAnnotationMirrors(), MustCallAlias.class)) { + // return true; + // } + // // Need to check the type mirror for ajava-derived annotations and the element itself + // // for human-written annotations from the source code. Getting to the ajava file + // // directly at this point is impossible, so we approximate "the ajava file has an + // // @MustCallAlias annotation" with "there is an @PolyMustCall annotation on the use + // // type, but not in the source code". This only works because none of our inference + // // techniques infer @PolyMustCall, so if @PolyMustCall is present but wasn't in the + // // source, it must have been derived from an @MustCallAlias annotation (which we do + // // infer). + // boolean ajavaFileHasMustCallAlias = + // useType.hasPrimaryAnnotation(PolyMustCall.class) + // && !atypeFactory.containsSameByClass(elt.getAnnotationMirrors(), + // PolyMustCall.class); + // if (ajavaFileHasMustCallAlias) { + // return true; + // } + // } + // return super.isValidUse(declarationType, useType, tree); + // } + + // TODO this might be important + // @Override + // protected boolean skipReceiverSubtypeCheck( + // MethodInvocationTree tree, + // AnnotatedTypeMirror methodDefinitionReceiver, + // AnnotatedTypeMirror methodCallReceiver) { + // // If you think of the receiver of the method call as an implicit parameter, it has some + // // MustCall type. For example, consider the method call: + // // void foo(@MustCall("bar") ThisClass this) + // // If we now call o.foo() where o has @MustCall({"bar, baz"}), the receiver subtype check + // // would throw an error, since o is not a subtype of @MustCall("bar"). However, since foo + // // cannot take ownership of its receiver, it does not matter what it 'thinks' the @MustCall + // // methods of the receiver are. Hence, it is always sound to skip this check. + // return true; + // } + + // TODO this might be a good solution if I run into this problem, since bot is default type + // /** + // * This method typically issues a warning if the result type of the constructor is not top, + // * because in top-default type systems that indicates a potential problem. The Must Call + // Checker + // * does not need this warning, because it expects the type of all constructors to be {@code + // * MustCall({})} (by default) or some other {@code MustCall} type, not the top type. + // * + // *

Instead, this method checks that the result type of a constructor is a supertype of the + // * declared type on the class, if one exists. + // * + // * @param constructorType an AnnotatedExecutableType for the constructor + // * @param constructorElement element that declares the constructor + // */ + // @Override + // protected void checkConstructorResult( + // AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + // AnnotatedTypeMirror defaultType = + // atypeFactory.getAnnotatedType(ElementUtils.enclosingTypeElement(constructorElement)); + // AnnotationMirror defaultAnno = defaultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + // AnnotatedTypeMirror resultType = constructorType.getReturnType(); + // AnnotationMirror resultAnno = resultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + // if (!qualHierarchy.isSubtypeShallow( + // defaultAnno, defaultType.getUnderlyingType(), resultAnno, + // resultType.getUnderlyingType())) { + // checker.reportError( + // constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); + // } + // } + + // /** + // * Change the default for exception parameter lower bounds to bottom (the default), to prevent + // * false positives. This is unsound; see the discussion on + // * https://github.com/typetools/checker-framework/issues/3839. + // * + // *

TODO: change checking of throws clauses to require that the thrown exception + // * is @MustCall({}). This would probably eliminate most of the same false positives, without + // * adding undue false positives. + // * + // * @return a set containing only the @MustCall({}) annotation + // */ + // @Override + // protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + // return new AnnotationMirrorSet(atypeFactory.BOTTOM); + // } + + // /** + // * Does not issue any warnings. + // * + // *

This implementation prevents recursing into annotation arguments. Annotation arguments + // are + // * literals, which don't have must-call obligations. + // * + // *

Annotation arguments are treated as return locations for the purposes of defaulting, + // rather + // * than parameter locations. This causes them to default incorrectly when the annotation is + // * defined in bytecode. See https://github.com/typetools/checker-framework/issues/3178 for an + // * explanation of why this is necessary to avoid false positives. + // */ + // @Override + // public Void visitAnnotation(AnnotationTree tree, Void p) { + // return null; + // } +} From 3a82c9c8f7dec1baff72ac53da0c524e335835e2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:44:37 +0100 Subject: [PATCH 021/374] add getMustCallAnnotatedTypeFactory to rlc utils --- .../resourceleak/ResourceLeakUtils.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 1957bc8c7c6a..a5dbe00a4ebf 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; import org.checkerframework.checker.nullness.qual.NonNull; @@ -71,6 +72,55 @@ public ResourceLeakUtils() {} } } + /** + * Given a type factory part of the resource leak ecosystem, returns the {@link + * MustCallAnnotatedTypeFactory} in the checker hierarchy. + * + * @param referenceAtf the type factory to retrieve the {@link MustCallAnnotatedTypeFactory} from + * @return the {@link MustCallAnnotatedTypeFactory} in the checker hierarchy + */ + public static @NonNull MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( + AnnotatedTypeFactory referenceAtf) { + if (referenceAtf == null) { + throw new IllegalArgumentException("Argument referenceAtf cannot be null"); + } else { + return getMustCallAnnotatedTypeFactory(referenceAtf.getChecker()); + } + } + + /** + * Given a checker part of the resource leak ecosystem, returns the {@link + * MustCallAnnotatedTypeFactory} in the checker hierarchy. + * + * @param referenceChecker the checker to retrieve the {@link MustCallAnnotatedTypeFactory} from + * @return the {@link MustCallAnnotatedTypeFactory} in the checker hierarchy + */ + public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( + SourceChecker referenceChecker) { + if (referenceChecker == null) { + throw new IllegalArgumentException("Argument referenceChecker cannot be null"); + } + + String className = referenceChecker.getClass().getSimpleName(); + if ("MustCallChecker".equals(className) + || "MustCallNoCreatesMustCallForChecker".equals(className)) { + return (MustCallAnnotatedTypeFactory) ((MustCallChecker) referenceChecker).getTypeFactory(); + } else if ("RLCCalledMethodsChecker".equals(className)) { + MustCallChecker mcc = referenceChecker.getSubchecker(MustCallChecker.class); + return getMustCallAnnotatedTypeFactory( + mcc != null + ? mcc + : referenceChecker.getSubchecker(MustCallNoCreatesMustCallForChecker.class)); + } else if ("ResourceLeakChecker".equals(className)) { + return getMustCallAnnotatedTypeFactory( + referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); + } else { + throw new IllegalArgumentException( + "Argument referenceChecker to ResourceLeakUtils#getMustCallAnnotatedTypeFactory(referenceChecker) expected to be an RLC checker but is " + + className); + } + } + /** * Given a type factory part of the resource leak ecosystem, returns the {@link * RLCCalledMethodsChecker} in the checker hierarchy. From f73a058290cde069d77fd3ae2d78892903b23be5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:45:17 +0100 Subject: [PATCH 022/374] add TOP as alias to NOTOWNINGCOLLECTION in coAtf --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 8cc5be8c1fe0..6bed597b4046 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -26,6 +26,9 @@ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@code @}{@link NotOwningCollection} annotation. */ + public final AnnotationMirror TOP; + + /** The {@code @}{@link NotOwningCollection} annotation. Equals TOP. */ public final AnnotationMirror NOTOWNINGCOLLECTION; /** The {@code @}{@link OwningCollection} annotation. */ @@ -48,6 +51,7 @@ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFa public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); NOTOWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, NotOwningCollection.class); + TOP = NOTOWNINGCOLLECTION; OWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, OwningCollection.class); OWNINGCOLLECTIONWITHOUTOBLIGATION = AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); From 6273d652ed71f3433fb879d5c6c144e785e9379c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:46:06 +0100 Subject: [PATCH 023/374] add createsourcevisitor and subcheckers to coc --- .../CollectionOwnershipChecker.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index 19360b3323b7..865d8b6887ad 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -1,10 +1,27 @@ package org.checkerframework.checker.collectionownership; +import java.util.Set; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.source.SourceChecker; /** * This typechecker tracks at most one owning variable per resource collection using an ownership - * type system. The resource leak checker verifies that the determined owner fulfills the calling - * obligations of its elements. + * type system. The resource leak checker verifies that (at least) the determined owner fulfills the + * calling obligations of its elements. */ -public class CollectionOwnershipChecker extends BaseTypeChecker {} +public class CollectionOwnershipChecker extends BaseTypeChecker { + + // TODO sck: not sure if this is necessary + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new CollectionOwnershipVisitor(this); + } + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + // checkers.add(RLCCalledMethodsChecker.class); + return checkers; + } +} From e6dd22a5bb7d5d14f3591abdb6a4726d70c83f02 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:47:00 +0100 Subject: [PATCH 024/374] add mcatf to coTransfer --- .../CollectionOwnershipTransfer.java | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 240955ea0d18..f876609351c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,14 +1,14 @@ package org.checkerframework.checker.collectionownership; import javax.lang.model.element.AnnotationMirror; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; -import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -26,6 +26,9 @@ public class CollectionOwnershipTransfer extends CFTransfer { /** The type factory. */ private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + /** The MustCall type factory to manage temp vars. */ + private final MustCallAnnotatedTypeFactory mcAtf; + /** * Create a CollectionOwnershipTransfer. * @@ -33,7 +36,8 @@ public class CollectionOwnershipTransfer extends CFTransfer { */ public CollectionOwnershipTransfer(CFAnalysis analysis) { super(analysis); - atypeFactory = (CollectionOwnershipTypeFactory) analysis.getTypeFactory(); + atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); + mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(atypeFactory); } @Override @@ -92,17 +96,19 @@ public TransferResult visitObjectCreation( return result; } - @Override - public TransferResult visitSwitchExpressionNode( - SwitchExpressionNode node, TransferInput input) { - TransferResult result = super.visitSwitchExpressionNode(node, input); - if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { - // Add the synthetic variable created during CFG construction to the temporary - // variable map (rather than creating a redundant temp var) - atypeFactory.tempVars.put(node.getTree(), node.getSwitchExpressionVar()); - } - return result; - } + // TODO sck: I think that I can use the temp var management from MC checker and don't + // need to replicate that in the COatf. If that turns out to not work, uncomment this + // @Override + // public TransferResult visitSwitchExpressionNode( + // SwitchExpressionNode node, TransferInput input) { + // TransferResult result = super.visitSwitchExpressionNode(node, input); + // if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // // Add the synthetic variable created during CFG construction to the temporary + // // variable map (rather than creating a redundant temp var) + // atypeFactory.tempVars.put(node.getTree(), node.getSwitchExpressionVar()); + // } + // return result; + // } /** * This method either creates or looks up the temp var t for node, and then updates the store to @@ -114,27 +120,25 @@ public TransferResult visitSwitchExpressionNode( public void updateStoreWithTempVar(TransferResult result, Node node) { // Must-call obligations on primitives are not supported. if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { - LocalVariableNode temp = getTempVar(node); + LocalVariableNode temp = mcAtf.getTempVar(node); if (temp != null) { + // atypeFactory.addTempVar(temp, node.getTree()); JavaExpression localExp = JavaExpression.fromNode(temp); AnnotationMirror anm = atypeFactory .getAnnotatedType(node.getTree()) .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); insertIntoStores(result, localExp, anm == null ? atypeFactory.TOP : anm); + // if (anm == null) { + // anm = atypeFactory.TOP; + // } + // if (result.containsTwoStores()) { + // result.getThenStore().insertValue(localExp, anm); + // result.getElseStore().insertValue(localExp, anm); + // } else { + // result.getRegularStore().insertValue(localExp, anm); + // } } } } - - /** - * Returns the temporary variable associated with node if it exists, or else null. - * - * @param node a node, which must be an expression (not a statement) - * @return a temporary variable node representing {@code node} that can be placed into a store or - * null if it doesn't exist - */ - private @Nullable LocalVariableNode getTempVar(Node node) { - // TODO sck: might have to query the mustcallAtf instead - return atypeFactory.tempVars.get(node.getTree()); - } } From bf2b83929f7932f8026f1e6a35b42c35decd5ead Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:47:25 +0100 Subject: [PATCH 025/374] fix typo --- .../checker/collectionownership/CollectionOwnershipVisitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 826447f2d569..fb2f7c1ae88a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -9,7 +9,7 @@ * the top type as the default type. */ public class CollectionOwnershipVisitor - extends BaseTypeVisitor { + extends BaseTypeVisitor { /** * Creates a new CollectionOwnershipVisitor. From ae6deccbca42089920680989579c1d8b1067cdf1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 10:52:25 +0100 Subject: [PATCH 026/374] insert co checker into rlc checker hierarchy --- .../CollectionOwnershipChecker.java | 3 ++- .../resourceleak/ResourceLeakChecker.java | 18 ++++++++++-------- .../resourceleak/ResourceLeakUtils.java | 11 ++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index 865d8b6887ad..240dfdb33319 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.collectionownership; import java.util.Set; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.source.SourceChecker; @@ -21,7 +22,7 @@ protected BaseTypeVisitor createSourceVisitor() { @Override protected Set> getImmediateSubcheckerClasses() { Set> checkers = super.getImmediateSubcheckerClasses(); - // checkers.add(RLCCalledMethodsChecker.class); + checkers.add(RLCCalledMethodsChecker.class); return checkers; } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index fb6b23387738..fb813dcb5f66 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -12,6 +12,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; +import org.checkerframework.checker.collectionownership.CollectionOwnershipChecker; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -24,16 +25,17 @@ /** * The entry point for the Resource Leak Checker. This checker only counts the number of {@link * org.checkerframework.checker.mustcall.qual.MustCall} annotations and defines a set of ignored - * exceptions. This checker calls the {@link RLCCalledMethodsChecker} as a direct subchecker, which - * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards traverses the cfg + * exceptions. This checker calls the {@link CollectionOwnershipChecker} as a direct subchecker, + * which then in turn calls the {@link RLCCalledMethodsChecker} as a subchecker, which calls the + * {@link MustCallChecker} as a subchecker. Afterwards, the consistency analyzer traverses the cfg * to check whether all MustCall obligations are fulfilled. * - *

The checker hierarchy is: this "empty" RLC → RLCCalledMethodsChecker → - * MustCallChecker + *

The checker hierarchy is: this "empty" RLC → CollectionOwnershipChecker → + * RLCCalledMethodsChecker → MustCallChecker * - *

The MustCallChecker is a subchecker of the RLCCm checker (instead of a sibling), since we want - * them to operate on the same cfg (so we can get both a CM and MC store for a given cfg block), - * which only works if one of them is a subchecker of the other. + *

The subchecker hierarchy is a line graph (instead of siblings), since we want them to operate + * on the same cfg (so we can get both a CM, MC, and CO store for a given cfg block), which only + * works if they are in a linear subchecker hierarchy. */ @SupportedOptions({ "permitStaticOwning", @@ -154,7 +156,7 @@ public ResourceLeakChecker() {} @Override protected Set> getSupportedCheckers() { Set> checkers = new LinkedHashSet<>(1); - checkers.add(RLCCalledMethodsChecker.class); + checkers.add(CollectionOwnershipChecker.class); return checkers; } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index a5dbe00a4ebf..07de5d12aec4 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.collectionownership.CollectionOwnershipChecker; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; @@ -25,6 +26,7 @@ public ResourceLeakUtils() {} new ArrayList<>( Arrays.asList( ResourceLeakChecker.class.getCanonicalName(), + CollectionOwnershipChecker.class.getCanonicalName(), RLCCalledMethodsChecker.class.getCanonicalName(), MustCallChecker.class.getCanonicalName(), MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); @@ -62,6 +64,7 @@ public ResourceLeakUtils() {} if ("ResourceLeakChecker".equals(className)) { return (ResourceLeakChecker) referenceChecker; } else if ("RLCCalledMethodsChecker".equals(className) + || "CollectionOwnershipChecker".equals(className) || "MustCallChecker".equals(className) || "MustCallNoCreatesMustCallForChecker".equals(className)) { return getResourceLeakChecker(referenceChecker.getParentChecker()); @@ -105,6 +108,9 @@ public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( if ("MustCallChecker".equals(className) || "MustCallNoCreatesMustCallForChecker".equals(className)) { return (MustCallAnnotatedTypeFactory) ((MustCallChecker) referenceChecker).getTypeFactory(); + } else if ("CollectionOwnershipChecker".equals(className)) { + return getMustCallAnnotatedTypeFactory( + referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); } else if ("RLCCalledMethodsChecker".equals(className)) { MustCallChecker mcc = referenceChecker.getSubchecker(MustCallChecker.class); return getMustCallAnnotatedTypeFactory( @@ -113,7 +119,7 @@ public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( : referenceChecker.getSubchecker(MustCallNoCreatesMustCallForChecker.class)); } else if ("ResourceLeakChecker".equals(className)) { return getMustCallAnnotatedTypeFactory( - referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); + referenceChecker.getSubchecker(CollectionOwnershipChecker.class)); } else { throw new IllegalArgumentException( "Argument referenceChecker to ResourceLeakUtils#getMustCallAnnotatedTypeFactory(referenceChecker) expected to be an RLC checker but is " @@ -154,6 +160,9 @@ public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( if ("RLCCalledMethodsChecker".equals(className)) { return (RLCCalledMethodsChecker) referenceChecker; } else if ("ResourceLeakChecker".equals(className)) { + return getRLCCalledMethodsChecker( + referenceChecker.getSubchecker(CollectionOwnershipChecker.class)); + } else if ("CollectionOwnershipChecker".equals(className)) { return getRLCCalledMethodsChecker( referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); } else if ("MustCallChecker".equals(className) From 2bd960f0c73b21741db10aef8e95851c985cf53d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 11:07:15 +0100 Subject: [PATCH 027/374] add subtypeof({}) to NotOwningCollection --- .../checker/collectionownership/qual/NotOwningCollection.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java index e0a2a82c49c8..8aca48f9912d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; /** * The top qualifier in the Collection Ownership type hierarchy. @@ -16,4 +17,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) public @interface NotOwningCollection {} From a94bfcdd7328cfc6e6d623432f7a8815b09767c5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 11:07:52 +0100 Subject: [PATCH 028/374] override constructor return consistency check for CollectionOwnership, since bottom is default --- .../CollectionOwnershipVisitor.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index fb2f7c1ae88a..99a0731d56b1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -1,7 +1,12 @@ package org.checkerframework.checker.collectionownership; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.ElementUtils; /** * The visitor for the Collection Ownership Checker. This visitor is similar to BaseTypeVisitor, but @@ -179,34 +184,32 @@ public CollectionOwnershipVisitor(BaseTypeChecker checker) { // } // TODO this might be a good solution if I run into this problem, since bot is default type - // /** - // * This method typically issues a warning if the result type of the constructor is not top, - // * because in top-default type systems that indicates a potential problem. The Must Call - // Checker - // * does not need this warning, because it expects the type of all constructors to be {@code - // * MustCall({})} (by default) or some other {@code MustCall} type, not the top type. - // * - // *

Instead, this method checks that the result type of a constructor is a supertype of the - // * declared type on the class, if one exists. - // * - // * @param constructorType an AnnotatedExecutableType for the constructor - // * @param constructorElement element that declares the constructor - // */ - // @Override - // protected void checkConstructorResult( - // AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { - // AnnotatedTypeMirror defaultType = - // atypeFactory.getAnnotatedType(ElementUtils.enclosingTypeElement(constructorElement)); - // AnnotationMirror defaultAnno = defaultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - // AnnotatedTypeMirror resultType = constructorType.getReturnType(); - // AnnotationMirror resultAnno = resultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - // if (!qualHierarchy.isSubtypeShallow( - // defaultAnno, defaultType.getUnderlyingType(), resultAnno, - // resultType.getUnderlyingType())) { - // checker.reportError( - // constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); - // } - // } + /** + * This method typically issues a warning if the result type of the constructor is not top, + * because in top-default type systems that indicates a potential problem. The Must Call Checker + * does not need this warning, because it expects the type of all constructors to be {@code + * OwningCollectionBottom} (by default). + * + *

Instead, this method checks that the result type of a constructor is a supertype of the + * declared type on the class, if one exists. + * + * @param constructorType an AnnotatedExecutableType for the constructor + * @param constructorElement element that declares the constructor + */ + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + AnnotatedTypeMirror defaultType = + atypeFactory.getAnnotatedType(ElementUtils.enclosingTypeElement(constructorElement)); + AnnotationMirror defaultAnno = defaultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + AnnotatedTypeMirror resultType = constructorType.getReturnType(); + AnnotationMirror resultAnno = resultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + if (!qualHierarchy.isSubtypeShallow( + defaultAnno, defaultType.getUnderlyingType(), resultAnno, resultType.getUnderlyingType())) { + checker.reportError( + constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); + } + } // /** // * Change the default for exception parameter lower bounds to bottom (the default), to prevent From c6661bdb112e0547f2473c1b49f2a7149cb70776 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 12:52:11 +0100 Subject: [PATCH 029/374] add qualifier hierarchy and subtype check in CollectionOwnershipQualifierHierarchy --- ...llectionOwnershipAnnotatedTypeFactory.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 6bed597b4046..8bac376dfbea 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -4,7 +4,9 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.util.Elements; @@ -162,7 +164,9 @@ public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ // } /** CollectionOwnership qualifier hierarchy. */ - private static class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { + protected class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { + + private Map, Integer> hierarchyLevel = new HashMap<>(); /** * Creates a NoElementQualifierHierarchy from the given classes. @@ -176,6 +180,29 @@ public CollectionOwnershipQualifierHierarchy( Elements elements, GenericAnnotatedTypeFactory atypeFactory) { super(qualifierClasses, elements, atypeFactory); + initializeHierarchyLevels(); + } + + private void initializeHierarchyLevels() { + hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.TOP.getClass(), 3); + hierarchyLevel.put( + CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION.getClass(), 2); + hierarchyLevel.put( + CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTIONWITHOUTOBLIGATION.getClass(), + 1); + hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.BOTTOM.getClass(), 0); + } + + private boolean isCollectionOwnershipQualifier(AnnotationMirror anno) { + return hierarchyLevel.containsKey(anno.getClass()); + } + + @Override + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { + if (isCollectionOwnershipQualifier(subAnno) && isCollectionOwnershipQualifier(superAnno)) { + return hierarchyLevel.get(subAnno.getClass()) <= hierarchyLevel.get(superAnno.getClass()); + } + return super.isSubtypeQualifiers(subAnno, superAnno); } } } From facbf25be5a68d050e25e5c43393d026b465bc8b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 12:52:36 +0100 Subject: [PATCH 030/374] make bottom default type for exception parameters --- .../collectionownership/qual/OwningCollectionBottom.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index c3aa62f54738..fba05b6c848d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -4,8 +4,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultFor; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom qualifier of the Collection Ownership type hierarchy, and the default qualifier. @@ -17,4 +19,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({OwningCollectionWithoutObligation.class}) @DefaultQualifierInHierarchy +@DefaultFor({TypeUseLocation.EXCEPTION_PARAMETER}) // , TypeUseLocation.UPPER_BOUND}) public @interface OwningCollectionBottom {} From 8f4b72a7377cebe54c40cd72b9224fe5c83a2c45 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 13:54:12 +0100 Subject: [PATCH 031/374] add doc for collection ownership qualifier hierarchy --- ...llectionOwnershipAnnotatedTypeFactory.java | 123 +++++++++--------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 8bac376dfbea..245dc0fe265b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -76,21 +76,6 @@ protected Set> createSupportedTypeQualifiers() { OwningCollectionBottom.class)); } - // @Override - // protected TreeAnnotator createTreeAnnotator() { - // return new ListTreeAnnotator(super.createTreeAnnotator(), new MustCallTreeAnnotator(this)); - // } - - // @Override - // protected TypeAnnotator createTypeAnnotator() { - // return new ListTypeAnnotator(super.createTypeAnnotator(), new MustCallTypeAnnotator(this)); - // } - - // @Override - // protected QualifierPolymorphism createQualifierPolymorphism() { - // return new MustCallQualifierPolymorphism(processingEnv, this); - // } - @Override protected QualifierHierarchy createQualifierHierarchy() { return new CollectionOwnershipQualifierHierarchy( @@ -113,6 +98,70 @@ public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); } + /** CollectionOwnership qualifier hierarchy. */ + protected class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { + + /** Maps Collection Ownership annotations to their hierarchy level. */ + private Map, Integer> hierarchyLevel = new HashMap<>(); + + /** + * Creates a NoElementQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + * @param atypeFactory the associated type factory + */ + public CollectionOwnershipQualifierHierarchy( + Collection> qualifierClasses, + Elements elements, + GenericAnnotatedTypeFactory atypeFactory) { + super(qualifierClasses, elements, atypeFactory); + hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.TOP.getClass(), 3); + hierarchyLevel.put( + CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION.getClass(), 2); + hierarchyLevel.put( + CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTIONWITHOUTOBLIGATION.getClass(), + 1); + hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.BOTTOM.getClass(), 0); + } + + /** + * Returns whether the given {@code AnnotationMirror} is part of the Collection Ownership + * qualifier hierarchy. + * + * @param anno the annotationmirror + * @return whether the given {@code AnnotationMirror} is part of the Collection Ownership + * qualifier hierarchy. + */ + private boolean isCollectionOwnershipQualifier(AnnotationMirror anno) { + return hierarchyLevel.containsKey(anno.getClass()); + } + + @Override + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { + if (isCollectionOwnershipQualifier(subAnno) && isCollectionOwnershipQualifier(superAnno)) { + return hierarchyLevel.get(subAnno.getClass()) <= hierarchyLevel.get(superAnno.getClass()); + } + return super.isSubtypeQualifiers(subAnno, superAnno); + } + } +} + + // @Override + // protected TreeAnnotator createTreeAnnotator() { + // return new ListTreeAnnotator(super.createTreeAnnotator(), new MustCallTreeAnnotator(this)); + // } + + // @Override + // protected TypeAnnotator createTypeAnnotator() { + // return new ListTypeAnnotator(super.createTypeAnnotator(), new MustCallTypeAnnotator(this)); + // } + + // @Override + // protected QualifierPolymorphism createQualifierPolymorphism() { + // return new MustCallQualifierPolymorphism(processingEnv, this); + // } + // /** // * The TreeAnnotator for the MustCall type system. // * @@ -162,47 +211,3 @@ public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ // return super.visitIdentifier(tree, type); // } // } - - /** CollectionOwnership qualifier hierarchy. */ - protected class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { - - private Map, Integer> hierarchyLevel = new HashMap<>(); - - /** - * Creates a NoElementQualifierHierarchy from the given classes. - * - * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy - * @param elements element utils - * @param atypeFactory the associated type factory - */ - public CollectionOwnershipQualifierHierarchy( - Collection> qualifierClasses, - Elements elements, - GenericAnnotatedTypeFactory atypeFactory) { - super(qualifierClasses, elements, atypeFactory); - initializeHierarchyLevels(); - } - - private void initializeHierarchyLevels() { - hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.TOP.getClass(), 3); - hierarchyLevel.put( - CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION.getClass(), 2); - hierarchyLevel.put( - CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTIONWITHOUTOBLIGATION.getClass(), - 1); - hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.BOTTOM.getClass(), 0); - } - - private boolean isCollectionOwnershipQualifier(AnnotationMirror anno) { - return hierarchyLevel.containsKey(anno.getClass()); - } - - @Override - public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (isCollectionOwnershipQualifier(subAnno) && isCollectionOwnershipQualifier(superAnno)) { - return hierarchyLevel.get(subAnno.getClass()) <= hierarchyLevel.get(superAnno.getClass()); - } - return super.isSubtypeQualifiers(subAnno, superAnno); - } - } -} From 05d7d3694d164d3d75cb875247f36899e085ba01 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 14:09:49 +0100 Subject: [PATCH 032/374] naively move consitency checker call into collection ownership type factory --- ...llectionOwnershipAnnotatedTypeFactory.java | 29 +++++++++++++++++++ .../RLCCalledMethodsAnnotatedTypeFactory.java | 21 -------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 245dc0fe265b..902f1cdd083f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -15,8 +15,15 @@ import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; +import org.checkerframework.checker.resourceleak.MustCallInference; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; @@ -145,6 +152,28 @@ public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror su return super.isSubtypeQualifiers(subAnno, superAnno); } } + + @Override + public void postAnalyze(ControlFlowGraph cfg) { + ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); + RLCCalledMethodsAnnotatedTypeFactory cmAtf = + (RLCCalledMethodsAnnotatedTypeFactory) + ResourceLeakUtils.getRLCCalledMethodsChecker(this).getTypeFactory(); + rlc.setRoot(root); + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); + mustCallConsistencyAnalyzer.analyze(cfg); + + // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for + // finalizer methods and @InheritableMustCall annotations for the class declarations. + if (cmAtf.getWholeProgramInference() != null) { + if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { + MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); + } + } + + super.postAnalyze(cfg); + // tempVarToTree.clear(); + } } // @Override diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 79b1260bfd1b..982ed3bf1036 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -29,8 +29,6 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; -import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; @@ -38,7 +36,6 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; -import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; @@ -137,24 +134,6 @@ public AnnotationMirror createCalledMethods(String... val) { return createAccumulatorAnnotation(Arrays.asList(val)); } - @Override - public void postAnalyze(ControlFlowGraph cfg) { - rlc.setRoot(root); - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); - mustCallConsistencyAnalyzer.analyze(cfg); - - // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for - // finalizer methods and @InheritableMustCall annotations for the class declarations. - if (getWholeProgramInference() != null) { - if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); - } - } - - super.postAnalyze(cfg); - tempVarToTree.clear(); - } - @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); From 651b55909046ed78408d98feca431d46eba45117 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 17 Mar 2025 14:38:14 +0100 Subject: [PATCH 033/374] make uniqueId counter in cfg builder static to ensure global uniqueness --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 44b2c714d939..ad3058224e9a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -882,7 +882,7 @@ protected void addLabelForNextNode(Label l) { /* --------------------------------------------------------- */ /** The UID for the next unique name. */ - protected long uid = 0; + protected static long uid = 0; /** * Returns a unique name starting with {@code prefix}. From 98c52a41a83ce6d55440537b5bea587d87d14763 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 07:58:17 +0100 Subject: [PATCH 034/374] add more defaulting locations to OCBottom --- .../qual/OwningCollectionBottom.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index fba05b6c848d..6fbaaad83062 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -19,5 +19,22 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({OwningCollectionWithoutObligation.class}) @DefaultQualifierInHierarchy -@DefaultFor({TypeUseLocation.EXCEPTION_PARAMETER}) // , TypeUseLocation.UPPER_BOUND}) +@DefaultFor({ + TypeUseLocation.ALL, + TypeUseLocation.CONSTRUCTOR_RESULT, + TypeUseLocation.EXCEPTION_PARAMETER, + TypeUseLocation.EXPLICIT_LOWER_BOUND, + TypeUseLocation.EXPLICIT_UPPER_BOUND, + TypeUseLocation.FIELD, + // TypeUseLocation.IMPLICIT_LOWER_BOUND, + TypeUseLocation.IMPLICIT_UPPER_BOUND, + TypeUseLocation.LOCAL_VARIABLE, + // TypeUseLocation.LOWER_BOUND, + TypeUseLocation.OTHERWISE, + // TypeUseLocation.PARAMETER, + TypeUseLocation.RECEIVER, + TypeUseLocation.RESOURCE_VARIABLE, + TypeUseLocation.RETURN, + TypeUseLocation.UPPER_BOUND +}) public @interface OwningCollectionBottom {} From ca3547038fdd0526184b6ba4eaa6c95480e7345c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 08:00:01 +0100 Subject: [PATCH 035/374] capitalize CFG for clarity --- .../checker/resourceleak/ResourceLeakChecker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index fb6b23387738..a725ae04ed39 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -25,14 +25,14 @@ * The entry point for the Resource Leak Checker. This checker only counts the number of {@link * org.checkerframework.checker.mustcall.qual.MustCall} annotations and defines a set of ignored * exceptions. This checker calls the {@link RLCCalledMethodsChecker} as a direct subchecker, which - * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards traverses the cfg + * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards traverses the CFG * to check whether all MustCall obligations are fulfilled. * *

The checker hierarchy is: this "empty" RLC → RLCCalledMethodsChecker → * MustCallChecker * *

The MustCallChecker is a subchecker of the RLCCm checker (instead of a sibling), since we want - * them to operate on the same cfg (so we can get both a CM and MC store for a given cfg block), + * them to operate on the same CFG (so we can get both a CM and MC store for a given CFG block), * which only works if one of them is a subchecker of the other. */ @SupportedOptions({ From 6b3e9d4a4ef3daa7c854604296a5d674d518b8d5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 08:31:48 +0100 Subject: [PATCH 036/374] replace hardcoding checker names with instanceof checks in rlc utils --- .../resourceleak/ResourceLeakUtils.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 1957bc8c7c6a..511ec9aa7d83 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -57,17 +57,15 @@ public ResourceLeakUtils() {} throw new IllegalArgumentException("Argument referenceChecker cannot be null"); } - String className = referenceChecker.getClass().getSimpleName(); - if ("ResourceLeakChecker".equals(className)) { + if (referenceChecker instanceof ResourceLeakChecker) { return (ResourceLeakChecker) referenceChecker; - } else if ("RLCCalledMethodsChecker".equals(className) - || "MustCallChecker".equals(className) - || "MustCallNoCreatesMustCallForChecker".equals(className)) { + } else if (referenceChecker instanceof RLCCalledMethodsChecker + || referenceChecker instanceof MustCallChecker) { return getResourceLeakChecker(referenceChecker.getParentChecker()); } else { throw new IllegalArgumentException( "Argument referenceChecker to ResourceLeakUtils#getResourceLeakChecker(referenceChecker) expected to be an RLC checker but is " - + className); + + referenceChecker.getClass().getSimpleName()); } } @@ -100,19 +98,17 @@ public ResourceLeakUtils() {} throw new IllegalArgumentException("Argument referenceChecker cannot be null"); } - String className = referenceChecker.getClass().getSimpleName(); - if ("RLCCalledMethodsChecker".equals(className)) { + if (referenceChecker instanceof RLCCalledMethodsChecker) { return (RLCCalledMethodsChecker) referenceChecker; - } else if ("ResourceLeakChecker".equals(className)) { + } else if (referenceChecker instanceof ResourceLeakChecker) { return getRLCCalledMethodsChecker( referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); - } else if ("MustCallChecker".equals(className) - || "MustCallNoCreatesMustCallForChecker".equals(className)) { + } else if (referenceChecker instanceof MustCallChecker) { return getRLCCalledMethodsChecker(referenceChecker.getParentChecker()); } else { throw new IllegalArgumentException( "Argument referenceChecker to ResourceLeakUtils#getRLCCalledMethodsChecker(referenceChecker) expected to be an RLC checker but is " - + className); + + referenceChecker.getClass().getSimpleName()); } } } From f3031c022e205919c760ec8f297e7738745db96c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 08:50:01 +0100 Subject: [PATCH 037/374] move isreturnsreceiverdisabled() to rlccmc from rlc --- .../checker/resourceleak/ResourceLeakChecker.java | 9 --------- .../rlccalledmethods/RLCCalledMethodsChecker.java | 9 +++++++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index a725ae04ed39..99f8553ac2fb 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -185,15 +185,6 @@ public void typeProcessingOver() { super.typeProcessingOver(); } - // /** - // * Disable the Returns Receiver Checker unless it has been explicitly enabled with the {@link - // * #ENABLE_RETURNS_RECEIVER} option. - // */ - // protected boolean isReturnsReceiverDisabled() { - // RLCCalledMethodsChecker rlccmc = ResourceLeakUtils.getRLCCalledMethodsChecker(this); - // return !hasOption(ENABLE_RETURNS_RECEIVER) || rlccmc.isReturnsReceiverDisabled(); - // } - /** * Get the set of exceptions that should be ignored. This set comes from the {@link * #IGNORED_EXCEPTIONS} option if it was provided, or {@link #DEFAULT_IGNORED_EXCEPTIONS} if not. diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 91535a1fa6be..958ace29380a 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -41,9 +41,14 @@ public SetOfTypes getIgnoredExceptions() { return getResourceLeakChecker().getIgnoredExceptions(); } + /** + * Disable the Returns Receiver Checker unless it has been explicitly enabled with the {@link + * ResourceLeakChecker#ENABLE_RETURNS_RECEIVER} option. + */ @Override - public boolean isReturnsReceiverDisabled() { - return super.isReturnsReceiverDisabled(); + protected boolean isReturnsReceiverDisabled() { + return !getResourceLeakChecker().hasOption(ResourceLeakChecker.ENABLE_RETURNS_RECEIVER) + || super.isReturnsReceiverDisabled(); } @Override From 5f8d78ed1e3caf31c2f36993b915f9c5321666d9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 08:51:43 +0100 Subject: [PATCH 038/374] capitalize CFG for clarity --- .../checker/rlccalledmethods/RLCCalledMethodsChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 958ace29380a..6f56b7db41b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -14,7 +14,7 @@ /** * The entry point for the RLCCalledMethodsChecker. This checker is a modifed {@link * CalledMethodsChecker} used as a subchecker in the ResourceLeakChecker, and never independently. - * Runs the MustCallChecker as a subchecker in order to share the cfg. + * Runs the MustCallChecker as a subchecker in order to share the CFG. */ @StubFiles("IOUtils.astub") public class RLCCalledMethodsChecker extends CalledMethodsChecker { From 9b61608d0b64b2f77d59f7298cf8d2502a12930b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 08:58:33 +0100 Subject: [PATCH 039/374] add throws declaration to checker getters in ResourceLeakUtils --- .../checker/resourceleak/ResourceLeakUtils.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 511ec9aa7d83..321d3a2968f0 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -34,6 +34,8 @@ public ResourceLeakUtils() {} * * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from * @return the {@link ResourceLeakChecker} in the checker hierarchy + * @throws IllegalArgumentException if {@code referenceAtf} is {@code null} or not part of the + * Resource Leak hierarchy */ public static @NonNull ResourceLeakChecker getResourceLeakChecker( AnnotatedTypeFactory referenceAtf) { @@ -50,6 +52,8 @@ public ResourceLeakUtils() {} * * @param referenceChecker the checker to retrieve the {@link ResourceLeakChecker} from * @return the {@link ResourceLeakChecker} in the checker hierarchy + * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the + * Resource Leak hierarchy */ public static @NonNull ResourceLeakChecker getResourceLeakChecker( SourceChecker referenceChecker) { @@ -75,6 +79,8 @@ public ResourceLeakUtils() {} * * @param referenceAtf the type factory to retrieve the {@link RLCCalledMethodsChecker} from * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy + * @throws IllegalArgumentException if {@code referenceAtf} is {@code null} or not part of the + * Resource Leak hierarchy */ public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( AnnotatedTypeFactory referenceAtf) { @@ -91,6 +97,8 @@ public ResourceLeakUtils() {} * * @param referenceChecker the checker to retrieve the {@link RLCCalledMethodsChecker} from * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy + * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the + * Resource Leak hierarchy */ public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( SourceChecker referenceChecker) { From 172383f856bde013130265d35777a300391bbb07 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 09:07:28 +0100 Subject: [PATCH 040/374] throw userError when rlc cannot be found in hiearchy in RLCCMC --- .../checker/rlccalledmethods/RLCCalledMethodsChecker.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 6f56b7db41b7..46b45ac3c9ef 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -10,6 +10,7 @@ import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.javacutil.UserError; /** * The entry point for the RLCCalledMethodsChecker. This checker is a modifed {@link @@ -72,7 +73,12 @@ protected Set> getImmediateSubcheckerClasses() { */ private ResourceLeakChecker getResourceLeakChecker() { if (this.rlc == null) { - this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); + try { + this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); + } catch (IllegalArgumentException e) { + throw new UserError( + "Cannot find ResourceLeakChecker in checker hierarchy. The RLCCalledMethods checker shouldn't be invoked directly, it is only a subchecker of the ResourceLeakChecker. Use the ResourceLeakChecker or the CalledMethodsChecker instead."); + } } return this.rlc; From 202da35e55364d6b153ad5889c4f99eb2b88d128 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 15:40:40 +0100 Subject: [PATCH 041/374] explicitly set fetch depth for misc jobs to 1000 --- .azure/azure-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index ef5461dfb521..5fea5c65c353 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -92,6 +92,7 @@ jobs: container: mdernst/cf-ubuntu-jdk11-plus:latest steps: - checkout: self + fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk17 @@ -103,6 +104,7 @@ jobs: container: mdernst/cf-ubuntu-jdk17-plus:latest steps: - checkout: self + fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk21 @@ -111,6 +113,7 @@ jobs: container: mdernst/cf-ubuntu-jdk21-plus:latest steps: - checkout: self + fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk23 @@ -119,6 +122,7 @@ jobs: container: mdernst/cf-ubuntu-jdk23-plus:latest steps: - checkout: self + fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh From 2c68b12d7f0c18410a968585b36c2074093ae0c4 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 18 Mar 2025 16:16:51 +0100 Subject: [PATCH 042/374] add doc for CollectionOwnershipChecker default constructor --- .../collectionownership/CollectionOwnershipChecker.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index 240dfdb33319..dde18fc23400 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -13,6 +13,9 @@ */ public class CollectionOwnershipChecker extends BaseTypeChecker { + /** Creates a CollectionOwnershipChecker. */ + public CollectionOwnershipChecker() {} + // TODO sck: not sure if this is necessary @Override protected BaseTypeVisitor createSourceVisitor() { From d070e83c7b3e5d08cbc7a7062f0a5b56476bbd4b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 19 Mar 2025 13:46:35 -0700 Subject: [PATCH 043/374] Error handling --- .../resourceleak/ResourceLeakUtils.java | 52 ++++++------------- .../RLCCalledMethodsAnnotatedTypeFactory.java | 2 +- .../RLCCalledMethodsVisitor.java | 2 +- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 321d3a2968f0..c006053da3bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -5,7 +5,6 @@ import java.util.List; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; -import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; @@ -16,8 +15,10 @@ */ public class ResourceLeakUtils { - /** Shouldn't be instantiated, pure utility class. */ - public ResourceLeakUtils() {} + /** Do not instantiate; this class is a collection of static methods. */ + private ResourceLeakUtils() { + throw new Error("Do not instantiate"); + } /** List of checker names associated with the Resource Leak Checker. */ public static List rlcCheckers = @@ -32,18 +33,12 @@ public ResourceLeakUtils() {} * Given a type factory part of the resource leak ecosystem, returns the {@link * ResourceLeakChecker} in the checker hierarchy. * - * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from + * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from; must be + * part of the Resource Leak hierarchy * @return the {@link ResourceLeakChecker} in the checker hierarchy - * @throws IllegalArgumentException if {@code referenceAtf} is {@code null} or not part of the - * Resource Leak hierarchy */ - public static @NonNull ResourceLeakChecker getResourceLeakChecker( - AnnotatedTypeFactory referenceAtf) { - if (referenceAtf == null) { - throw new IllegalArgumentException("Argument referenceAtf cannot be null"); - } else { - return getResourceLeakChecker(referenceAtf.getChecker()); - } + public static ResourceLeakChecker getResourceLeakChecker(AnnotatedTypeFactory referenceAtf) { + return getResourceLeakChecker(referenceAtf.getChecker()); } /** @@ -55,12 +50,7 @@ public ResourceLeakUtils() {} * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the * Resource Leak hierarchy */ - public static @NonNull ResourceLeakChecker getResourceLeakChecker( - SourceChecker referenceChecker) { - if (referenceChecker == null) { - throw new IllegalArgumentException("Argument referenceChecker cannot be null"); - } - + public static ResourceLeakChecker getResourceLeakChecker(SourceChecker referenceChecker) { if (referenceChecker instanceof ResourceLeakChecker) { return (ResourceLeakChecker) referenceChecker; } else if (referenceChecker instanceof RLCCalledMethodsChecker @@ -68,8 +58,8 @@ public ResourceLeakUtils() {} return getResourceLeakChecker(referenceChecker.getParentChecker()); } else { throw new IllegalArgumentException( - "Argument referenceChecker to ResourceLeakUtils#getResourceLeakChecker(referenceChecker) expected to be an RLC checker but is " - + referenceChecker.getClass().getSimpleName()); + "Bad argument to ResourceLeakUtils#getResourceLeakChecker(): " + + (referenceChecker == null ? "null" : referenceChecker.getClass().getSimpleName())); } } @@ -82,13 +72,9 @@ public ResourceLeakUtils() {} * @throws IllegalArgumentException if {@code referenceAtf} is {@code null} or not part of the * Resource Leak hierarchy */ - public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( + public static RLCCalledMethodsChecker getRLCCalledMethodsChecker( AnnotatedTypeFactory referenceAtf) { - if (referenceAtf == null) { - throw new IllegalArgumentException("Argument referenceAtf cannot be null"); - } else { - return getRLCCalledMethodsChecker(referenceAtf.getChecker()); - } + return getRLCCalledMethodsChecker(referenceAtf.getChecker()); } /** @@ -100,12 +86,7 @@ public ResourceLeakUtils() {} * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the * Resource Leak hierarchy */ - public static @NonNull RLCCalledMethodsChecker getRLCCalledMethodsChecker( - SourceChecker referenceChecker) { - if (referenceChecker == null) { - throw new IllegalArgumentException("Argument referenceChecker cannot be null"); - } - + public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker referenceChecker) { if (referenceChecker instanceof RLCCalledMethodsChecker) { return (RLCCalledMethodsChecker) referenceChecker; } else if (referenceChecker instanceof ResourceLeakChecker) { @@ -115,8 +96,9 @@ public ResourceLeakUtils() {} return getRLCCalledMethodsChecker(referenceChecker.getParentChecker()); } else { throw new IllegalArgumentException( - "Argument referenceChecker to ResourceLeakUtils#getRLCCalledMethodsChecker(referenceChecker) expected to be an RLC checker but is " - + referenceChecker.getClass().getSimpleName()); + "Bad argument to" + + " ResourceLeakUtils#getRLCCalledMethodsChecker(): " + + (referenceChecker == null ? "null" : referenceChecker.getClass().getSimpleName())); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 79b1260bfd1b..291de3500213 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -61,7 +61,7 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotatedTypeFactory implements CreatesMustCallForElementSupplier { - /** The rlc parent checker. */ + /** The RLC parent checker. */ private ResourceLeakChecker rlc; /** The MustCall.value element/field. */ diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java index 356735fd221b..9bb2c80ddf67 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java @@ -380,7 +380,7 @@ public static List getCreatesMustCallForValues( * Get all {@link EnsuresCalledMethods} annotations on an element. * * @param elt an executable element that might have {@link EnsuresCalledMethods} annotations - * @param atypeFactory a RLCCalledMethodsAnnotatedTypeFactory + * @param atypeFactory a {@link RLCCalledMethodsAnnotatedTypeFactory} * @return a set of {@link EnsuresCalledMethods} annotations */ @Pure From 774b17b0100018646fe348aa79de55075987e0a2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 19 Mar 2025 13:49:58 -0700 Subject: [PATCH 044/374] Error handling --- .../resourceleak/ResourceLeakUtils.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index c006053da3bc..6454105bb47c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -30,7 +30,7 @@ private ResourceLeakUtils() { MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); /** - * Given a type factory part of the resource leak ecosystem, returns the {@link + * Given a type factory that is part of the resource leak ecosystem, returns the {@link * ResourceLeakChecker} in the checker hierarchy. * * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from; must be @@ -42,13 +42,12 @@ public static ResourceLeakChecker getResourceLeakChecker(AnnotatedTypeFactory re } /** - * Given a checker part of the resource leak ecosystem, returns the {@link ResourceLeakChecker} in - * the checker hierarchy. + * Given a checker that is part of the resource leak ecosystem, returns the {@link + * ResourceLeakChecker} in the checker hierarchy. * - * @param referenceChecker the checker to retrieve the {@link ResourceLeakChecker} from + * @param referenceChecker the checker to retrieve the {@link ResourceLeakChecker} from; must be + * part of the Resource Leak hierarchy * @return the {@link ResourceLeakChecker} in the checker hierarchy - * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the - * Resource Leak hierarchy */ public static ResourceLeakChecker getResourceLeakChecker(SourceChecker referenceChecker) { if (referenceChecker instanceof ResourceLeakChecker) { @@ -64,13 +63,12 @@ public static ResourceLeakChecker getResourceLeakChecker(SourceChecker reference } /** - * Given a type factory part of the resource leak ecosystem, returns the {@link + * Given a type factory that is part of the resource leak ecosystem, returns the {@link * RLCCalledMethodsChecker} in the checker hierarchy. * - * @param referenceAtf the type factory to retrieve the {@link RLCCalledMethodsChecker} from + * @param referenceAtf the type factory to retrieve the {@link RLCCalledMethodsChecker} from; must + * be part of the Resource Leak hierarchy * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy - * @throws IllegalArgumentException if {@code referenceAtf} is {@code null} or not part of the - * Resource Leak hierarchy */ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker( AnnotatedTypeFactory referenceAtf) { @@ -78,13 +76,12 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker( } /** - * Given a checker part of the resource leak ecosystem, returns the {@link + * Given a checker that is part of the resource leak ecosystem, returns the {@link * RLCCalledMethodsChecker} in the checker hierarchy. * - * @param referenceChecker the checker to retrieve the {@link RLCCalledMethodsChecker} from + * @param referenceChecker the checker to retrieve the {@link RLCCalledMethodsChecker} from; must + * be part of the Resource Leak hierarchy * @return the {@link RLCCalledMethodsChecker} in the checker hierarchy - * @throws IllegalArgumentException if {@code referenceChecker} is {@code null} or not part of the - * Resource Leak hierarchy */ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker referenceChecker) { if (referenceChecker instanceof RLCCalledMethodsChecker) { From aa3aac29e29019282f63aa9e8c87e97eaccaed0b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 19 Mar 2025 13:55:11 -0700 Subject: [PATCH 045/374] Comment tweaks --- .../checker/resourceleak/ResourceLeakChecker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index 99f8553ac2fb..5471c00dbd5d 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -25,8 +25,8 @@ * The entry point for the Resource Leak Checker. This checker only counts the number of {@link * org.checkerframework.checker.mustcall.qual.MustCall} annotations and defines a set of ignored * exceptions. This checker calls the {@link RLCCalledMethodsChecker} as a direct subchecker, which - * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards traverses the CFG - * to check whether all MustCall obligations are fulfilled. + * then in turn calls the {@link MustCallChecker} as a subchecker, and afterwards this checker + * traverses the CFG to check whether all MustCall obligations are fulfilled. * *

The checker hierarchy is: this "empty" RLC → RLCCalledMethodsChecker → * MustCallChecker @@ -186,7 +186,7 @@ public void typeProcessingOver() { } /** - * Get the set of exceptions that should be ignored. This set comes from the {@link + * Returns the set of exceptions that should be ignored. This set comes from the {@link * #IGNORED_EXCEPTIONS} option if it was provided, or {@link #DEFAULT_IGNORED_EXCEPTIONS} if not. * * @return the set of exceptions to ignore From 074234067f5f156752bab1d055f0da47dbec3dfd Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 21 Mar 2025 16:12:06 +0100 Subject: [PATCH 046/374] replace illegalargumentexception with typesystemerror --- .../checker/resourceleak/ResourceLeakUtils.java | 3 ++- .../checker/rlccalledmethods/RLCCalledMethodsChecker.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 6454105bb47c..cb2e76b0585f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -8,6 +8,7 @@ import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.javacutil.TypeSystemError; /** * Collection of static utility functions related to the various (sub-) checkers within the @@ -92,7 +93,7 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker r } else if (referenceChecker instanceof MustCallChecker) { return getRLCCalledMethodsChecker(referenceChecker.getParentChecker()); } else { - throw new IllegalArgumentException( + throw new TypeSystemError( "Bad argument to" + " ResourceLeakUtils#getRLCCalledMethodsChecker(): " + (referenceChecker == null ? "null" : referenceChecker.getClass().getSimpleName())); diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 46b45ac3c9ef..512d742c7575 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -10,6 +10,7 @@ import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; /** @@ -75,7 +76,7 @@ private ResourceLeakChecker getResourceLeakChecker() { if (this.rlc == null) { try { this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); - } catch (IllegalArgumentException e) { + } catch (TypeSystemError e) { throw new UserError( "Cannot find ResourceLeakChecker in checker hierarchy. The RLCCalledMethods checker shouldn't be invoked directly, it is only a subchecker of the ResourceLeakChecker. Use the ResourceLeakChecker or the CalledMethodsChecker instead."); } From 8716f0be1226b5c2742290d00f0366d3c205d97a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 21 Mar 2025 16:12:54 +0100 Subject: [PATCH 047/374] improve terminology consistency in comments --- .../checker/resourceleak/ResourceLeakUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index cb2e76b0585f..b9d50178758c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -31,7 +31,7 @@ private ResourceLeakUtils() { MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); /** - * Given a type factory that is part of the resource leak ecosystem, returns the {@link + * Given a type factory that is part of the resource leak checker hierarchy, returns the {@link * ResourceLeakChecker} in the checker hierarchy. * * @param referenceAtf the type factory to retrieve the {@link ResourceLeakChecker} from; must be @@ -43,7 +43,7 @@ public static ResourceLeakChecker getResourceLeakChecker(AnnotatedTypeFactory re } /** - * Given a checker that is part of the resource leak ecosystem, returns the {@link + * Given a checker that is part of the resource leak checker hierarchy, returns the {@link * ResourceLeakChecker} in the checker hierarchy. * * @param referenceChecker the checker to retrieve the {@link ResourceLeakChecker} from; must be @@ -57,14 +57,14 @@ public static ResourceLeakChecker getResourceLeakChecker(SourceChecker reference || referenceChecker instanceof MustCallChecker) { return getResourceLeakChecker(referenceChecker.getParentChecker()); } else { - throw new IllegalArgumentException( + throw new TypeSystemError( "Bad argument to ResourceLeakUtils#getResourceLeakChecker(): " + (referenceChecker == null ? "null" : referenceChecker.getClass().getSimpleName())); } } /** - * Given a type factory that is part of the resource leak ecosystem, returns the {@link + * Given a type factory that is part of the resource leak checker hierarchy, returns the {@link * RLCCalledMethodsChecker} in the checker hierarchy. * * @param referenceAtf the type factory to retrieve the {@link RLCCalledMethodsChecker} from; must @@ -77,7 +77,7 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker( } /** - * Given a checker that is part of the resource leak ecosystem, returns the {@link + * Given a checker that is part of the resource leak checker hierarchy, returns the {@link * RLCCalledMethodsChecker} in the checker hierarchy. * * @param referenceChecker the checker to retrieve the {@link RLCCalledMethodsChecker} from; must From 013a0de683f6d0924065933323290feabae6cd66 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 21 Mar 2025 16:59:06 +0100 Subject: [PATCH 048/374] add monotonicnonnull annotation --- .../checker/rlccalledmethods/RLCCalledMethodsChecker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index 512d742c7575..b0a3f03a3a3e 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -4,6 +4,7 @@ import org.checkerframework.checker.calledmethods.CalledMethodsChecker; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.resourceleak.SetOfTypes; @@ -25,7 +26,7 @@ public class RLCCalledMethodsChecker extends CalledMethodsChecker { public RLCCalledMethodsChecker() {} /** The parent resource leak checker */ - private ResourceLeakChecker rlc; + private @MonotonicNonNull ResourceLeakChecker rlc; @Override protected BaseTypeVisitor createSourceVisitor() { From dea96d17563c49d3fc5a7956c47f54c2efe9fd90 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 24 Mar 2025 10:19:36 -0700 Subject: [PATCH 049/374] Remove `@NonNull` --- .../checker/resourceleak/ResourceLeakUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index c26761943f7a..3d5cbf6da189 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -75,7 +75,7 @@ public static ResourceLeakChecker getResourceLeakChecker(SourceChecker reference * @param referenceAtf the type factory to retrieve the {@link MustCallAnnotatedTypeFactory} from * @return the {@link MustCallAnnotatedTypeFactory} in the checker hierarchy */ - public static @NonNull MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( + public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( AnnotatedTypeFactory referenceAtf) { if (referenceAtf == null) { throw new IllegalArgumentException("Argument referenceAtf cannot be null"); From b6ae0bcf6a248b841793fd66a208293bc7054e1c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 24 Mar 2025 10:21:34 -0700 Subject: [PATCH 050/374] Fix merge --- .../checker/resourceleak/ResourceLeakUtils.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 3d5cbf6da189..fe169a45a750 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -56,10 +56,10 @@ public static ResourceLeakChecker getResourceLeakChecker(AnnotatedTypeFactory re public static ResourceLeakChecker getResourceLeakChecker(SourceChecker referenceChecker) { if (referenceChecker instanceof ResourceLeakChecker) { return (ResourceLeakChecker) referenceChecker; - } else if ("RLCCalledMethodsChecker".equals(className) - || "CollectionOwnershipChecker".equals(className) - || "MustCallChecker".equals(className) - || "MustCallNoCreatesMustCallForChecker".equals(className)) { + } else if (referenceChecker instanceof RLCCalledMethodsChecker + || referenceChecker instanceof CollectionOwnershipChecker + || referenceChecker instanceof MustCallChecker + || referenceChecker instanceof MustCallNoCreatesMustCallForChecker) { return getResourceLeakChecker(referenceChecker.getParentChecker()); } else { throw new TypeSystemError( @@ -115,7 +115,9 @@ public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( referenceChecker.getSubchecker(CollectionOwnershipChecker.class)); } else { throw new IllegalArgumentException( - "Argument referenceChecker to ResourceLeakUtils#getMustCallAnnotatedTypeFactory(referenceChecker) expected to be an RLC checker but is " + "Argument referenceChecker to" + + " ResourceLeakUtils#getMustCallAnnotatedTypeFactory(referenceChecker) expected to" + + " be an RLC checker but is " + className); } } From 9bee4ea23aad726c9f977d9fe61d7b2c2e5efcff Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 24 Mar 2025 10:22:18 -0700 Subject: [PATCH 051/374] Fix merge --- .../checker/resourceleak/ResourceLeakUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index fe169a45a750..e6723836a9e8 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -149,7 +149,7 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker r } else if (referenceChecker instanceof ResourceLeakChecker) { return getRLCCalledMethodsChecker( referenceChecker.getSubchecker(CollectionOwnershipChecker.class)); - } else if ("CollectionOwnershipChecker".equals(className)) { + } else if (referenceChecker instanceof CollectionOwnershipChecker) { return getRLCCalledMethodsChecker( referenceChecker.getSubchecker(RLCCalledMethodsChecker.class)); } else if (referenceChecker instanceof MustCallChecker) { From c617f6836d299a4588fdb53e99243a730289e604 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 24 Mar 2025 20:44:22 +0100 Subject: [PATCH 052/374] explicitly set fetchDepth to 0 for misc jobs, disabling shallow fetch --- .azure/azure-pipelines.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.azure/azure-pipelines.yml b/.azure/azure-pipelines.yml index ef5461dfb521..7a2f28ece2d4 100644 --- a/.azure/azure-pipelines.yml +++ b/.azure/azure-pipelines.yml @@ -83,6 +83,7 @@ jobs: # Unlimited fetchDepth for misc_jobs, because of need to make contributors.tex +# explicit fetchDepth of 0 disables shallow fetch and fetches full commit history - job: misc_jdk11 dependsOn: - canary_jobs @@ -92,6 +93,7 @@ jobs: container: mdernst/cf-ubuntu-jdk11-plus:latest steps: - checkout: self + fetchDepth: 0 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk17 @@ -103,6 +105,7 @@ jobs: container: mdernst/cf-ubuntu-jdk17-plus:latest steps: - checkout: self + fetchDepth: 0 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk21 @@ -111,6 +114,7 @@ jobs: container: mdernst/cf-ubuntu-jdk21-plus:latest steps: - checkout: self + fetchDepth: 0 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk23 @@ -119,6 +123,7 @@ jobs: container: mdernst/cf-ubuntu-jdk23-plus:latest steps: - checkout: self + fetchDepth: 0 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh From be7441ed24db4acfd5ccb50076b83774f404ce1a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 28 Apr 2025 18:12:40 +0200 Subject: [PATCH 053/374] debug logs --- .../rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 6f9a7abee868..9ed8794062f2 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -145,8 +145,11 @@ public void postAnalyze(ControlFlowGraph cfg) { // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. + rlc.reportWarning(root, "has wpi? " + getWholeProgramInference()); if (getWholeProgramInference() != null) { + rlc.reportWarning(root, "cmAtf has wpi"); if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { + rlc.reportWarning(root, "running mci"); MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); } } From 3776bdeaebe48f9d1b09691da583f0daa681d5d7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 28 Apr 2025 18:24:33 +0200 Subject: [PATCH 054/374] add logs --- .../CollectionOwnershipAnnotatedTypeFactory.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 902f1cdd083f..ca0364cbc808 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -165,8 +165,11 @@ public void postAnalyze(ControlFlowGraph cfg) { // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. + rlc.reportWarning(root, "has wpi? " + cmAtf.getWholeProgramInference()); if (cmAtf.getWholeProgramInference() != null) { + rlc.reportWarning(root, "cmAtf has wpi"); if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { + rlc.reportWarning(root, "running mci"); MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); } } From 4e440d61544eb627d99c279b5e768b09a947c27f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:11:41 -0700 Subject: [PATCH 055/374] Add diagnostics --- checker/build.gradle | 5 ++++- .../framework/type/AnnotatedTypeFactory.java | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/checker/build.gradle b/checker/build.gradle index 6cfee73f537f..ecb9300cbcdd 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -840,6 +840,7 @@ task ainferTest(group: 'Verification') { // // This is run as part of the inferenceTests task. +// The tests run are in checker/tests/wpi-many/testin.txt . task wpiManyTest(type: Exec, group: 'Verification') { description = 'Tests the wpi-many.sh script (and indirectly the wpi.sh script). Requires an Internet connection.' dependsOn(assembleForJavac) @@ -945,7 +946,9 @@ task wpiManyTest(type: Exec, group: 'Verification') { // Ignore the summary line that reports the total number of warnings (which can be single or plural). || line.endsWith(' warning') || line.endsWith(' warnings') - || line.startsWith('warning: No processor claimed any of these annotations: ')) { + || line.startsWith('warning: No processor claimed any of these annotations: ') + // Ignore debugging output created with this specific prefix + || line.startsWith('DEBUG: ')) { return; } if (!line.trim().equals('')) { 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 79b12118cbb1..d15f6f1f80cf 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -614,6 +614,10 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.annotationFormatter = createAnnotationFormatter(); this.typeInformationPresenter = createTypeInformationPresenter(); + String msg = + String.format("DEBUG: hasOption(infer) = %s for %s%n", checker.hasOption("infer"), checker); + System.out.println(msg); + System.out.printf(msg); if (checker.hasOption("infer")) { checkInvalidOptionsInferSignatures(); String inferArg = checker.getOption("infer"); From 247807a3752464b39d5b16aa76062f9499ac4e0b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:13:41 -0700 Subject: [PATCH 056/374] Diagnostics --- .../checkerframework/framework/type/AnnotatedTypeFactory.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d15f6f1f80cf..d0abe035f6be 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -615,9 +615,8 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.typeInformationPresenter = createTypeInformationPresenter(); String msg = - String.format("DEBUG: hasOption(infer) = %s for %s%n", checker.hasOption("infer"), checker); + String.format("DEBUG: hasOption(infer) = %s for %s", checker.hasOption("infer"), checker); System.out.println(msg); - System.out.printf(msg); if (checker.hasOption("infer")) { checkInvalidOptionsInferSignatures(); String inferArg = checker.getOption("infer"); From 910c93766e3edb4dc0b3b35874781e8b7c187909 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:18:40 -0700 Subject: [PATCH 057/374] Diagnostics --- checker/build.gradle | 5 ++++- .../framework/type/AnnotatedTypeFactory.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/checker/build.gradle b/checker/build.gradle index 6cfee73f537f..ecb9300cbcdd 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -840,6 +840,7 @@ task ainferTest(group: 'Verification') { // // This is run as part of the inferenceTests task. +// The tests run are in checker/tests/wpi-many/testin.txt . task wpiManyTest(type: Exec, group: 'Verification') { description = 'Tests the wpi-many.sh script (and indirectly the wpi.sh script). Requires an Internet connection.' dependsOn(assembleForJavac) @@ -945,7 +946,9 @@ task wpiManyTest(type: Exec, group: 'Verification') { // Ignore the summary line that reports the total number of warnings (which can be single or plural). || line.endsWith(' warning') || line.endsWith(' warnings') - || line.startsWith('warning: No processor claimed any of these annotations: ')) { + || line.startsWith('warning: No processor claimed any of these annotations: ') + // Ignore debugging output created with this specific prefix + || line.startsWith('DEBUG: ')) { return; } if (!line.trim().equals('')) { 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 79b12118cbb1..d0abe035f6be 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -614,6 +614,9 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.annotationFormatter = createAnnotationFormatter(); this.typeInformationPresenter = createTypeInformationPresenter(); + String msg = + String.format("DEBUG: hasOption(infer) = %s for %s", checker.hasOption("infer"), checker); + System.out.println(msg); if (checker.hasOption("infer")) { checkInvalidOptionsInferSignatures(); String inferArg = checker.getOption("infer"); From 5b4bd9d5fa9ecbcb2d235bec69a59afbdde863ca Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:22:58 -0700 Subject: [PATCH 058/374] Diagnostics --- checker/build.gradle | 3 ++- .../RLCCalledMethodsAnnotatedTypeFactory.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/checker/build.gradle b/checker/build.gradle index ecb9300cbcdd..747256b189e8 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -948,7 +948,8 @@ task wpiManyTest(type: Exec, group: 'Verification') { || line.endsWith(' warnings') || line.startsWith('warning: No processor claimed any of these annotations: ') // Ignore debugging output created with this specific prefix - || line.startsWith('DEBUG: ')) { + || line.startsWith('DEBUG: ') + || line.startsWith('warning: DEBUG: ')) { return; } if (!line.trim().equals('')) { diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 9ed8794062f2..d8be612bd4c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -145,11 +145,11 @@ public void postAnalyze(ControlFlowGraph cfg) { // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. - rlc.reportWarning(root, "has wpi? " + getWholeProgramInference()); + rlc.reportWarning(root, "DEBUG: has wpi? " + getWholeProgramInference()); if (getWholeProgramInference() != null) { - rlc.reportWarning(root, "cmAtf has wpi"); + rlc.reportWarning(root, "DEBUG: cmAtf has wpi"); if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - rlc.reportWarning(root, "running mci"); + rlc.reportWarning(root, "DEBUG: running mci"); MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); } } From f331df91787db690e2891f81cb8e890d77ab1ff1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:27:00 -0700 Subject: [PATCH 059/374] More ignoring --- checker/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/build.gradle b/checker/build.gradle index 747256b189e8..41d0237b025e 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -949,7 +949,8 @@ task wpiManyTest(type: Exec, group: 'Verification') { || line.startsWith('warning: No processor claimed any of these annotations: ') // Ignore debugging output created with this specific prefix || line.startsWith('DEBUG: ') - || line.startsWith('warning: DEBUG: ')) { + || line.startsWith('warning: DEBUG: ') + || line.startsWith('warning: [DEBUG: ')) { return; } if (!line.trim().equals('')) { From 9edc0c0f09cad29db54f67964305ff3d71ea542a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:28:11 -0700 Subject: [PATCH 060/374] Broaden --- checker/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/build.gradle b/checker/build.gradle index 41d0237b025e..458c5a431dd0 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -950,7 +950,7 @@ task wpiManyTest(type: Exec, group: 'Verification') { // Ignore debugging output created with this specific prefix || line.startsWith('DEBUG: ') || line.startsWith('warning: DEBUG: ') - || line.startsWith('warning: [DEBUG: ')) { + || line.contains('warning: [DEBUG: ')) { return; } if (!line.trim().equals('')) { From d3106bec9db06d1f71659950996d73f26094ec58 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:29:31 -0700 Subject: [PATCH 061/374] println, not reportWarning --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index d8be612bd4c1..0ff565c9244e 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -145,11 +145,11 @@ public void postAnalyze(ControlFlowGraph cfg) { // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. - rlc.reportWarning(root, "DEBUG: has wpi? " + getWholeProgramInference()); + System.out.println("DEBUG: has wpi? " + getWholeProgramInference()); if (getWholeProgramInference() != null) { - rlc.reportWarning(root, "DEBUG: cmAtf has wpi"); + System.out.println("DEBUG: cmAtf has wpi"); if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - rlc.reportWarning(root, "DEBUG: running mci"); + System.out.println("DEBUG: running mci"); MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); } } From 0884fd2a5ab9312bc3466422e06c41e8097804d6 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 28 Apr 2025 10:56:37 -0700 Subject: [PATCH 062/374] Better diagnostics --- checker/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/build.gradle b/checker/build.gradle index 458c5a431dd0..26f486e9e491 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -906,7 +906,7 @@ task wpiManyTest(type: Exec, group: 'Verification') { } logFiles.visit { FileVisitDetails details -> def filename = "${typecheckFilesDir}" + details.getName() - println("======== start of contents of ${filename} ========") + println("======== start of contents of ${filename}, which is TYPECHECKING output, not WPI output ========") details.getFile().eachLine { line -> println(line) } println("======== end of contents of ${filename} ========") } @@ -954,7 +954,7 @@ task wpiManyTest(type: Exec, group: 'Verification') { return; } if (!line.trim().equals('')) { - println("======== start of contents of ${filename} ========") + println("======== start of contents of ${filename}, which is TYPECHECKING output, not WPI output ========") details.getFile().eachLine { l -> println(l) } println("======== end of contents of ${filename} ========") throw new GradleException('Failure: WPI scripts produced an unexpected output in ' + filename + '. ' + From 88560dcd7f73a07fafdae3e6faa315010896674f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 29 Apr 2025 13:06:31 +0200 Subject: [PATCH 063/374] remove some logs --- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 3254366d65cc..7c4d216b8b22 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -165,9 +165,7 @@ public void postAnalyze(ControlFlowGraph cfg) { // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. - rlc.reportWarning(root, "DEBUG: has wpi? " + cmAtf.getWholeProgramInference()); if (cmAtf.getWholeProgramInference() != null) { - rlc.reportWarning(root, "DEBUG: cmAtf has wpi"); if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { rlc.reportWarning(root, "DEBUG: running mci"); MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); From 63a83a4f201c71ff4c871db89683e92a3d36efcc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 29 Apr 2025 16:18:18 +0200 Subject: [PATCH 064/374] replace warning with printstmt --- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 7c4d216b8b22..e299115ff923 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -167,7 +167,7 @@ public void postAnalyze(ControlFlowGraph cfg) { // finalizer methods and @InheritableMustCall annotations for the class declarations. if (cmAtf.getWholeProgramInference() != null) { if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - rlc.reportWarning(root, "DEBUG: running mci"); + System.out.println("DEBUG: running mci"); MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); } } From 593f6f26e49c186ec5e9b7d14a80e24ed6fad8c2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 4 May 2025 09:39:09 -0700 Subject: [PATCH 065/374] Comment about using testin.txt file --- checker/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/build.gradle b/checker/build.gradle index 26f486e9e491..1a1cb9603295 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -840,7 +840,8 @@ task ainferTest(group: 'Verification') { // // This is run as part of the inferenceTests task. -// The tests run are in checker/tests/wpi-many/testin.txt . +// File checker/tests/wpi-many/testin.txt lists the tests that are run. To +// debug one WPI test, comment out all but one line of that file. task wpiManyTest(type: Exec, group: 'Verification') { description = 'Tests the wpi-many.sh script (and indirectly the wpi.sh script). Requires an Internet connection.' dependsOn(assembleForJavac) From 002f3e7361fd62bc7835c4c0eea38a24447ce041 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 5 May 2025 22:34:27 +0200 Subject: [PATCH 066/374] remove debug logs --- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 -- .../checkerframework/framework/type/AnnotatedTypeFactory.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index e299115ff923..ece064060e15 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -167,13 +167,11 @@ public void postAnalyze(ControlFlowGraph cfg) { // finalizer methods and @InheritableMustCall annotations for the class declarations. if (cmAtf.getWholeProgramInference() != null) { if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - System.out.println("DEBUG: running mci"); MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); } } super.postAnalyze(cfg); - // tempVarToTree.clear(); } } 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 d0abe035f6be..79b12118cbb1 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -614,9 +614,6 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { this.annotationFormatter = createAnnotationFormatter(); this.typeInformationPresenter = createTypeInformationPresenter(); - String msg = - String.format("DEBUG: hasOption(infer) = %s for %s", checker.hasOption("infer"), checker); - System.out.println(msg); if (checker.hasOption("infer")) { checkInvalidOptionsInferSignatures(); String inferArg = checker.getOption("infer"); From dadccdf8949d6bcbeaea181721f44cb37c940bfb Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 5 May 2025 22:34:51 +0200 Subject: [PATCH 067/374] add getter method for CollectionOwnershipAtf --- .../resourceleak/ResourceLeakUtils.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index bddd59a9a90a..40c4e7773bab 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; + +import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipChecker; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -160,4 +162,54 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker r + (referenceChecker == null ? "null" : referenceChecker.getClass().getSimpleName())); } } + + /** + * Given a checker part of the resource leak ecosystem, returns the {@link + * CollectionOwnershipAnnotatedTypeFactory} in the checker hierarchy. + * + * @param referenceChecker the checker to retrieve the {@link CollectionOwnershipAnnotatedTypeFactory} from + * @return the {@link CollectionOwnershipAnnotatedTypeFactory} in the checker hierarchy + */ + public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnnotatedTypeFactory(SourceChecker referenceChecker) { + if (referenceChecker == null) { + throw new IllegalArgumentException("Argument referenceChecker cannot be null"); + } + + String className = referenceChecker.getClass().getSimpleName(); + if ("CollectionOwnershipChecker".equals(className) + || "MustCallNoCreatesMustCallForChecker".equals(className)) { + return (CollectionOwnershipAnnotatedTypeFactory) ((CollectionOwnershipChecker) referenceChecker).getTypeFactory(); + } else if ("RLCCalledMethodsChecker".equals(className)) { + return getCollectionOwnershipAnnotatedTypeFactory( + referenceChecker.getParentChecker()); + } else if ("MustCallChecker".equals(className)) { + return getCollectionOwnershipAnnotatedTypeFactory( + referenceChecker.getParentChecker()); + } else if ("ResourceLeakChecker".equals(className)) { + return getCollectionOwnershipAnnotatedTypeFactory( + referenceChecker.getSubchecker(CollectionOwnershipChecker.class)); + } else { + throw new IllegalArgumentException( + "Argument referenceChecker to" + + " ResourceLeakUtils#getCollectionOwnershipAnnotatedTypeFactory(referenceChecker) expected to" + + " be an RLC checker but is " + + className); + } + } + + /** + * Given a type factory part of the resource leak ecosystem, returns the {@link + * CollectionOwnershipAnnotatedTypeFactory} in the checker hierarchy. + * + * @param referenceAtf the type factory to retrieve the {@link CollectionOwnershipAnnotatedTypeFactory} from + * @return the {@link CollectionOwnershipAnnotatedTypeFactory} in the checker hierarchy + */ + public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnnotatedTypeFactory( + AnnotatedTypeFactory referenceAtf) { + if (referenceAtf == null) { + throw new IllegalArgumentException("Argument referenceAtf cannot be null"); + } else { + return getCollectionOwnershipAnnotatedTypeFactory(referenceAtf.getChecker()); + } + } } From 9eff51b307407852032ab32856b58604d8976f12 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 5 May 2025 22:36:00 +0200 Subject: [PATCH 068/374] access wpi object of CollectionOwnershipChecker in MCI (vs the one from RLCCMC) --- .../checker/resourceleak/MustCallInference.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java index 2721931722b0..ba275118109e 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java @@ -24,6 +24,7 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; import org.checkerframework.checker.mustcall.qual.MustCallAlias; import org.checkerframework.checker.mustcall.qual.NotOwning; @@ -311,7 +312,8 @@ private void analyzeReturnNode(Set obligations, ReturnNode node) { * {@literal @InheritableMustCall} annotations to the enclosing class. */ private void addMemberAndClassAnnotations() { - WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + CollectionOwnershipAnnotatedTypeFactory coAtf = ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(resourceLeakAtf); + WholeProgramInference wpi = coAtf.getWholeProgramInference(); assert wpi != null : "MustCallInference is running without WPI."; for (VariableElement fieldElt : getOwningFields()) { wpi.addFieldDeclarationAnnotation(fieldElt, OWNING); From ca85a4d23d0775590f855a4b608d515403e278d4 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 5 May 2025 22:40:25 +0200 Subject: [PATCH 069/374] uncomment tests again (were commented for debugging) --- checker/tests/wpi-many/testin.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/checker/tests/wpi-many/testin.txt b/checker/tests/wpi-many/testin.txt index 81639aa4afc0..cb28c0c3d075 100644 --- a/checker/tests/wpi-many/testin.txt +++ b/checker/tests/wpi-many/testin.txt @@ -1,7 +1,7 @@ -# https://github.com/kelloggm/wpi-many-tests-bcel-util 91415634c1d47ada93de4fb9105bbd7817928a58 -# https://github.com/kelloggm/wpi-many-tests-bibtex-clean fe7ae9a276d8d392c174d497c14a7bf4f30876e8 -# https://github.com/kelloggm/wpi-many-tests-ensures-called-methods e5ad4c582392ee8edd8a6270b12aad876df94782 -# https://github.com/kelloggm/wpi-many-tests-html-pretty-print de5790094a1b3cece9cd97152a5c54afa615e6d9 -# https://github.com/kelloggm/-wpi-many-tests-bibtex-clean 4da53d845f91e5a34987f70227ec6e5b11e8fc60 -# # This comment line tests that the commenting feature works (if it doesn't, then this line will be read and fail, as it's not a URL). +https://github.com/kelloggm/wpi-many-tests-bcel-util 91415634c1d47ada93de4fb9105bbd7817928a58 +https://github.com/kelloggm/wpi-many-tests-bibtex-clean fe7ae9a276d8d392c174d497c14a7bf4f30876e8 +https://github.com/kelloggm/wpi-many-tests-ensures-called-methods e5ad4c582392ee8edd8a6270b12aad876df94782 +https://github.com/kelloggm/wpi-many-tests-html-pretty-print de5790094a1b3cece9cd97152a5c54afa615e6d9 +https://github.com/kelloggm/-wpi-many-tests-bibtex-clean 4da53d845f91e5a34987f70227ec6e5b11e8fc60 +# This comment line tests that the commenting feature works (if it doesn't, then this line will be read and fail, as it's not a URL). https://github.com/Nargeshdb/wpi-many-tests-owning-field f446e987fdb87a4c8364c9a2728be70900cb30d5 From 45ba71071d20072a2ff9a966984a348a16a3f632 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 6 May 2025 12:18:13 +0200 Subject: [PATCH 070/374] suppress the faulty upper bound warning --- .../initializedfields/InitializedFieldsAnnotatedTypeFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java index 73d44e0eb60a..98c8bb816bf6 100644 --- a/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java @@ -220,6 +220,7 @@ private String[] fieldsToInitialize(TypeElement type) { * @param field a field * @return true if the default field value is consistent with the field's declared type */ + @SuppressWarnings("collectionownership:argument") private boolean defaultValueIsOK(VariableElement field) { if (defaultValueAtypeFactories.isEmpty()) { return false; From ecff3362e85f0fa1773e5195f6dd494cf9371be1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 6 May 2025 14:07:09 +0200 Subject: [PATCH 071/374] reduce OwningCollectionBottom defaults to a minimum --- .../qual/OwningCollectionBottom.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index 6fbaaad83062..22517e63185a 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -20,19 +20,19 @@ @SubtypeOf({OwningCollectionWithoutObligation.class}) @DefaultQualifierInHierarchy @DefaultFor({ - TypeUseLocation.ALL, - TypeUseLocation.CONSTRUCTOR_RESULT, + // TypeUseLocation.ALL, + // TypeUseLocation.CONSTRUCTOR_RESULT, TypeUseLocation.EXCEPTION_PARAMETER, - TypeUseLocation.EXPLICIT_LOWER_BOUND, - TypeUseLocation.EXPLICIT_UPPER_BOUND, - TypeUseLocation.FIELD, + // TypeUseLocation.EXPLICIT_LOWER_BOUND, + // TypeUseLocation.EXPLICIT_UPPER_BOUND, + // TypeUseLocation.FIELD, // TypeUseLocation.IMPLICIT_LOWER_BOUND, - TypeUseLocation.IMPLICIT_UPPER_BOUND, + // TypeUseLocation.IMPLICIT_UPPER_BOUND, TypeUseLocation.LOCAL_VARIABLE, // TypeUseLocation.LOWER_BOUND, - TypeUseLocation.OTHERWISE, + // TypeUseLocation.OTHERWISE, // TypeUseLocation.PARAMETER, - TypeUseLocation.RECEIVER, + // TypeUseLocation.RECEIVER, TypeUseLocation.RESOURCE_VARIABLE, TypeUseLocation.RETURN, TypeUseLocation.UPPER_BOUND From bc03ae3f16cfaf246f30b9bc1f87b5ddfcb04981 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 14 May 2025 19:01:04 +0200 Subject: [PATCH 072/374] debug stmts --- .../checkerframework/framework/type/AnnotatedTypeFactory.java | 1 + .../framework/util/defaults/QualifierDefaults.java | 1 + 2 files changed, 2 insertions(+) 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 79b12118cbb1..ba7005b9513c 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -1355,6 +1355,7 @@ public AnnotatedTypeMirror getAnnotatedType(Element elt) { // Annotations explicitly written in the source code, // or obtained from bytecode. AnnotatedTypeMirror type = fromElement(elt); + System.out.println("Debug: getAnnotatedType: " + elt + " of type " + type + "\n"); addComputedTypeAnnotations(elt, type); return type; } diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java index acb71786a4c2..889a399f929e 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java @@ -391,6 +391,7 @@ private boolean conflictsWithExistingDefaults( */ public void annotate(Element elt, AnnotatedTypeMirror type) { if (elt != null) { + // System.out.println("Debug: annotate: " + elt + " of type " + type + "\n"); switch (elt.getKind()) { case FIELD: case LOCAL_VARIABLE: From 6f7c2eb170390f2436b072e0aa05b808312bacbb Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 16 May 2025 16:32:05 +0200 Subject: [PATCH 073/374] fix type var upper bound issue --- .../CollectionOwnershipAnnotatedTypeFactory.java | 6 ------ .../InitializedFieldsAnnotatedTypeFactory.java | 1 - 2 files changed, 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ece064060e15..8ae4a02b4644 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -83,12 +83,6 @@ protected Set> createSupportedTypeQualifiers() { OwningCollectionBottom.class)); } - @Override - protected QualifierHierarchy createQualifierHierarchy() { - return new CollectionOwnershipQualifierHierarchy( - this.getSupportedTypeQualifiers(), elements, this); - } - /** * Fetches the store from the results of dataflow for {@code first}. If {@code afterFirstStore} is * true, then the store after {@code first} is returned; if {@code afterFirstStore} is false, the diff --git a/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java index 98c8bb816bf6..73d44e0eb60a 100644 --- a/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/common/initializedfields/InitializedFieldsAnnotatedTypeFactory.java @@ -220,7 +220,6 @@ private String[] fieldsToInitialize(TypeElement type) { * @param field a field * @return true if the default field value is consistent with the field's declared type */ - @SuppressWarnings("collectionownership:argument") private boolean defaultValueIsOK(VariableElement field) { if (defaultValueAtypeFactories.isEmpty()) { return false; From 3f6aaa90dcc27e8c39157fb82af03da41b6cd685 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 17 May 2025 17:45:19 +0200 Subject: [PATCH 074/374] remove debug stmts --- .../checkerframework/framework/type/AnnotatedTypeFactory.java | 1 - .../framework/util/defaults/QualifierDefaults.java | 1 - 2 files changed, 2 deletions(-) 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 ba7005b9513c..79b12118cbb1 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -1355,7 +1355,6 @@ public AnnotatedTypeMirror getAnnotatedType(Element elt) { // Annotations explicitly written in the source code, // or obtained from bytecode. AnnotatedTypeMirror type = fromElement(elt); - System.out.println("Debug: getAnnotatedType: " + elt + " of type " + type + "\n"); addComputedTypeAnnotations(elt, type); return type; } diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java index 889a399f929e..acb71786a4c2 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java @@ -391,7 +391,6 @@ private boolean conflictsWithExistingDefaults( */ public void annotate(Element elt, AnnotatedTypeMirror type) { if (elt != null) { - // System.out.println("Debug: annotate: " + elt + " of type " + type + "\n"); switch (elt.getKind()) { case FIELD: case LOCAL_VARIABLE: From 7ecc3b94a3ac9af0a515e49d6cdfc196dfc9b3de Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 17 May 2025 17:45:52 +0200 Subject: [PATCH 075/374] remove faulty qualifier hierarchy of co checker --- ...llectionOwnershipAnnotatedTypeFactory.java | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 8ae4a02b4644..cca54bc9feac 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -99,54 +99,6 @@ public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); } - /** CollectionOwnership qualifier hierarchy. */ - protected class CollectionOwnershipQualifierHierarchy extends NoElementQualifierHierarchy { - - /** Maps Collection Ownership annotations to their hierarchy level. */ - private Map, Integer> hierarchyLevel = new HashMap<>(); - - /** - * Creates a NoElementQualifierHierarchy from the given classes. - * - * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy - * @param elements element utils - * @param atypeFactory the associated type factory - */ - public CollectionOwnershipQualifierHierarchy( - Collection> qualifierClasses, - Elements elements, - GenericAnnotatedTypeFactory atypeFactory) { - super(qualifierClasses, elements, atypeFactory); - hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.TOP.getClass(), 3); - hierarchyLevel.put( - CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION.getClass(), 2); - hierarchyLevel.put( - CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTIONWITHOUTOBLIGATION.getClass(), - 1); - hierarchyLevel.put(CollectionOwnershipAnnotatedTypeFactory.this.BOTTOM.getClass(), 0); - } - - /** - * Returns whether the given {@code AnnotationMirror} is part of the Collection Ownership - * qualifier hierarchy. - * - * @param anno the annotationmirror - * @return whether the given {@code AnnotationMirror} is part of the Collection Ownership - * qualifier hierarchy. - */ - private boolean isCollectionOwnershipQualifier(AnnotationMirror anno) { - return hierarchyLevel.containsKey(anno.getClass()); - } - - @Override - public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (isCollectionOwnershipQualifier(subAnno) && isCollectionOwnershipQualifier(superAnno)) { - return hierarchyLevel.get(subAnno.getClass()) <= hierarchyLevel.get(superAnno.getClass()); - } - return super.isSubtypeQualifiers(subAnno, superAnno); - } - } - @Override public void postAnalyze(ControlFlowGraph cfg) { ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); From 93656e219b9d5dd01947c83d50a175b31fd3d5bd Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 17 May 2025 17:46:18 +0200 Subject: [PATCH 076/374] make bottom default for exception params in co checker --- .../CollectionOwnershipVisitor.java | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 99a0731d56b1..8e590e9b21df 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -6,6 +6,7 @@ import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.ElementUtils; /** @@ -25,6 +26,45 @@ public CollectionOwnershipVisitor(BaseTypeChecker checker) { super(checker); } + /** + * This method typically issues a warning if the result type of the constructor is not top, + * because in top-default type systems that indicates a potential problem. The Must Call Checker + * does not need this warning, because it expects the type of all constructors to be {@code + * OwningCollectionBottom} (by default). + * + *

Instead, this method checks that the result type of a constructor is a supertype of the + * declared type on the class, if one exists. + * + * @param constructorType an AnnotatedExecutableType for the constructor + * @param constructorElement element that declares the constructor + */ + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + AnnotatedTypeMirror defaultType = + atypeFactory.getAnnotatedType(ElementUtils.enclosingTypeElement(constructorElement)); + AnnotationMirror defaultAnno = defaultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + AnnotatedTypeMirror resultType = constructorType.getReturnType(); + AnnotationMirror resultAnno = resultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); + if (!qualHierarchy.isSubtypeShallow( + defaultAnno, defaultType.getUnderlyingType(), resultAnno, resultType.getUnderlyingType())) { + checker.reportError( + constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); + } + } + + /** + * Change the default for exception parameter lower bounds to bottom (the default), to prevent + * false positives. + * + * @return a set containing only the Bottom annotation + */ + @Override + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(atypeFactory.BOTTOM); + } + + // TODO maybe check contravariance for parameters and covariance for return types here // (and invariance for fields) // @Override @@ -183,50 +223,6 @@ public CollectionOwnershipVisitor(BaseTypeChecker checker) { // return true; // } - // TODO this might be a good solution if I run into this problem, since bot is default type - /** - * This method typically issues a warning if the result type of the constructor is not top, - * because in top-default type systems that indicates a potential problem. The Must Call Checker - * does not need this warning, because it expects the type of all constructors to be {@code - * OwningCollectionBottom} (by default). - * - *

Instead, this method checks that the result type of a constructor is a supertype of the - * declared type on the class, if one exists. - * - * @param constructorType an AnnotatedExecutableType for the constructor - * @param constructorElement element that declares the constructor - */ - @Override - protected void checkConstructorResult( - AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { - AnnotatedTypeMirror defaultType = - atypeFactory.getAnnotatedType(ElementUtils.enclosingTypeElement(constructorElement)); - AnnotationMirror defaultAnno = defaultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - AnnotatedTypeMirror resultType = constructorType.getReturnType(); - AnnotationMirror resultAnno = resultType.getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - if (!qualHierarchy.isSubtypeShallow( - defaultAnno, defaultType.getUnderlyingType(), resultAnno, resultType.getUnderlyingType())) { - checker.reportError( - constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); - } - } - - // /** - // * Change the default for exception parameter lower bounds to bottom (the default), to prevent - // * false positives. This is unsound; see the discussion on - // * https://github.com/typetools/checker-framework/issues/3839. - // * - // *

TODO: change checking of throws clauses to require that the thrown exception - // * is @MustCall({}). This would probably eliminate most of the same false positives, without - // * adding undue false positives. - // * - // * @return a set containing only the @MustCall({}) annotation - // */ - // @Override - // protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { - // return new AnnotationMirrorSet(atypeFactory.BOTTOM); - // } - // /** // * Does not issue any warnings. // * From 53385dc1c600117d9126102ef13ddab7ad71e00f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 17 May 2025 17:56:31 +0200 Subject: [PATCH 077/374] fix formatting issue --- .../CollectionOwnershipAnnotatedTypeFactory.java | 7 ------- .../collectionownership/CollectionOwnershipVisitor.java | 1 - 2 files changed, 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index cca54bc9feac..53a954caba37 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -3,13 +3,9 @@ import com.sun.source.tree.CompilationUnitTree; import java.lang.annotation.Annotation; import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.util.Elements; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; @@ -26,9 +22,6 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; -import org.checkerframework.framework.type.NoElementQualifierHierarchy; -import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; /** The annotated type factory for the Collection Ownership Checker. */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 8e590e9b21df..d5a0d095f2d0 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -64,7 +64,6 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { return new AnnotationMirrorSet(atypeFactory.BOTTOM); } - // TODO maybe check contravariance for parameters and covariance for return types here // (and invariance for fields) // @Override From 0a22d88d2333c234b17941a88bec73f5855d31fa Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 24 May 2025 17:26:37 +0200 Subject: [PATCH 078/374] add isCollection(TypeMirror) to rlcutils --- .../resourceleak/ResourceLeakUtils.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index c6adca45fda3..05ce18edb0cd 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -2,7 +2,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipChecker; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; @@ -11,7 +14,9 @@ import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.TypesUtils; /** * Collection of static utility functions related to the various (sub-) checkers within the @@ -213,4 +218,35 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno return getCollectionOwnershipAnnotatedTypeFactory(referenceAtf.getChecker()); } } + + /** + * Returns whether the given Element is a java.util.Collection type by checking whether the raw + * type of the element is assignable from java.util.Collection. Returns false if element is null, + * or has no valid type. + * + * @param element the element + * @param atf an AnnotatedTypeFactory to get the annotated type of the element + * @return whether the given element is a Java.util.Collection type + */ + public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { + if (element == null) return false; + AnnotatedTypeMirror elementTypeMirror = atf.getAnnotatedType(element).getErased(); + if (elementTypeMirror == null || elementTypeMirror.getUnderlyingType() == null) return false; + return isCollection(elementTypeMirror.getUnderlyingType()); + } + + /** + * Returns whether the given {@link TypeMirror} is a java.util.Collection subclass. This is + * determined by getting the class of the TypeMirror and checking whether it is assignable from + * java.util.Collection. + * + * @param type the TypeMirror + * @return whether type is a java.util.Collection + */ + public static boolean isCollection(TypeMirror type) { + if (type == null) return false; + Class elementRawType = TypesUtils.getClassFromType(type); + if (elementRawType == null) return false; + return Collection.class.isAssignableFrom(elementRawType); + } } From 9f62d27dd32c526631ce98ce88cf53e8fb975b72 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 24 May 2025 17:27:19 +0200 Subject: [PATCH 079/374] collectionownership default test cases --- .../test/junit/ResourceLeakCollections.java | 25 ++++++ .../CollectionOwnershipDefaults.java | 87 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java create mode 100644 checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java new file mode 100644 index 000000000000..644846d85925 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakCollections extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakCollections(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-collections"}; + } +} diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java new file mode 100644 index 000000000000..c6e8037ff91a --- /dev/null +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -0,0 +1,87 @@ +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +/* + * Test whether the defaults of resource collection fields, parameters, return types and new allocations + * are as expected. + */ +class CollectionOwnershipDefaults { + + int n = 10; + + /* This should default to @OwningCollection */ + Collection resourceCollectionField; + + /* + * Check that resource collection field defaults to @OwningCollection. + */ + void checkResourceCollectionFieldDefault() { + checkArgIsOwning(resourceCollectionField); + // :: error: argument + checkArgIsOCwoO(resourceCollectionField); + } + + /* + * This method checks that its parameter defaults to @NotOwningCollection. + + * Parameter should default to @NotOwningCollection. + * Return type should default to @OwningCollection. + */ + List identity(List list) { + // list : @NotOwningCollection. Thus, next line should throw an error. + + // :: error: argument + checkArgIsOwning(list); + + return list; + } + + /* + * Checks that an @OwningCollection can be passed to a resource collection parameter (because it should default + * to @OwningCollection). I.e. check that the resource collection parameter default is visible at call-site as well. + */ + void checkResourceCollectionParameterDefault() { + @OwningCollection List list = new ArrayList(); + identity(list); + } + + /* + * Checks that return value of a resource collection correctly defaults to @OwningCollection + */ + void checkResourceCollectionReturnValueDefault() { + List returnVal = identity(new ArrayList()); + // returnVal supposed to be @OwningCollection. Thus, first call should succeed, second fail. + + checkArgIsOwning(returnVal); + // :: error: argument + checkArgIsOCwoO(returnVal); + } + + /* + * Checks that a newly allocated resource collection has type @OwningCollection. + */ + void checkNewResourceCollectionDefault() { + List newResourceCollection = new ArrayList(); + checkArgIsOwning(newResourceCollection); + // :: error: argument + checkArgIsOCwoO(newResourceCollection); + + Socket[] newResourceArray = new Socket[n]; + checkArgIsOwning(newResourceArray); + // :: error: argument + checkArgIsOCwoO(newResourceArray); + } + + void checkArgIsOwning(@OwningCollection Collection collection) {} + + void checkArgIsOwning(@OwningCollection Socket[] collection) {} + + void checkArgIsOCwoO(@OwningCollectionWithoutObligation Collection collection) {} + + void checkArgIsOCwoO(@OwningCollectionWithoutObligation Socket[] collection) {} +} From b19be0fa5392974070958f09ffe09804c79864a1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 24 May 2025 17:27:45 +0200 Subject: [PATCH 080/374] type annotator for collectionownership defaults --- ...llectionOwnershipAnnotatedTypeFactory.java | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 53a954caba37..524b75cc8fab 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -22,6 +22,10 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; /** The annotated type factory for the Collection Ownership Checker. */ @@ -112,17 +116,59 @@ public void postAnalyze(ControlFlowGraph cfg) { super.postAnalyze(cfg); } -} - // @Override - // protected TreeAnnotator createTreeAnnotator() { - // return new ListTreeAnnotator(super.createTreeAnnotator(), new MustCallTreeAnnotator(this)); - // } + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator( + super.createTypeAnnotator(), new CollectionOwnershipTypeAnnotator(this)); + } + + private static class CollectionOwnershipTypeAnnotator extends TypeAnnotator { + + /** + * Constructor matching super. + * + * @param atypeFactory the type factory + */ + public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + @Override + public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { + AnnotatedTypeMirror returnType = t.getReturnType(); + // check whether return type is resource collection + + boolean returnTypeIsCollection = + ResourceLeakUtils.isCollection(returnType.getUnderlyingType()); + // boolean returnTypeIsArray = + // returnType.getKind() == TypeKind.ARRAY; + if (returnTypeIsCollection) { + // System.out.println(t.getClass()); + } + // AnnotatedTypeMirror componentType = + // returnTypeIsArray ? ((AnnotatedArrayType) returnType).getComponentType() + // : (returnTypeIsCollection ? ) + // ; + return super.visitExecutable(t, p); + } + } + + // /* + // * The bulk of adding computed type annotations happens in the other overload + // * addComputedTypeAnnotations(Element, AnnotatedTypeMirror). + // * Here, we change the return type of methods annotated CollectionAlias to + // MustCallOnElementsUnknown, + // * such that at call-site, the returned alias will be guarded by the proper restrictions. + // * + // * Also the type of fields when they are accessed. + // */ // @Override - // protected TypeAnnotator createTypeAnnotator() { - // return new ListTypeAnnotator(super.createTypeAnnotator(), new MustCallTypeAnnotator(this)); + // public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { + // super.addComputedTypeAnnotations(tree, type, useFlow); // } +} // @Override // protected QualifierPolymorphism createQualifierPolymorphism() { From 88bf84a2071850b37122b507f386811ee0738bd5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 12:43:06 +0200 Subject: [PATCH 081/374] helper method to get mustcall values of typemirror --- .../resourceleak/ResourceLeakUtils.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 05ce18edb0cd..8188aa423175 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,18 +3,28 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipChecker; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; @@ -249,4 +259,49 @@ public static boolean isCollection(TypeMirror type) { if (elementRawType == null) return false; return Collection.class.isAssignableFrom(elementRawType); } + + /** + * Returns the list of mustcall obligations for the given {@code TypeMirror} upper bound (either + * the type variable itself if it is concrete or the upper bound if its a wildcard or generic). + * + *

If the type variable has no upper bound, for instance if it is a wildcard with no extends + * clause the method returns null + * + * @param type the {@code TypeMirror} + * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type + * @return the list of mustcall obligations for the upper bound of {@code type} or null if the + * upper bound is null. + */ + public static @Nullable List getMcValues( + TypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { + if (type instanceof TypeVariable) { + // a generic - replace with upper bound and return null if it has no upper bound + type = ((TypeVariable) type).getUpperBound(); + if (type == null) { + return null; + } + } else if (type instanceof WildcardType) { + // a wildcard - replace with upper bound and return null if it has no upper bound + type = ((WildcardType) type).getExtendsBound(); + if (type == null) { + return null; + } + } + TypeElement typeElement = TypesUtils.getTypeElement(type); + AnnotationMirror imcAnnotation = + mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); + AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); + Set mcValues = new HashSet<>(); + if (mcAnnotation != null) { + mcValues.addAll( + AnnotationUtils.getElementValueArray( + mcAnnotation, mcAtf.getMustCallValueElement(), String.class)); + } + if (imcAnnotation != null) { + mcValues.addAll( + AnnotationUtils.getElementValueArray( + imcAnnotation, mcAtf.getInheritableMustCallValueElement(), String.class)); + } + return new ArrayList<>(mcValues); + } } From 30fdfea49196260cc459ccb5cdd1a1071ac2a13d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 12:43:28 +0200 Subject: [PATCH 082/374] default resource collection return types to OwningCollection --- ...llectionOwnershipAnnotatedTypeFactory.java | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 524b75cc8fab..2ec9521b5d5f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -4,12 +4,15 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeKind; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.MustCallInference; @@ -24,6 +27,8 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; @@ -123,7 +128,7 @@ protected TypeAnnotator createTypeAnnotator() { super.createTypeAnnotator(), new CollectionOwnershipTypeAnnotator(this)); } - private static class CollectionOwnershipTypeAnnotator extends TypeAnnotator { + private class CollectionOwnershipTypeAnnotator extends TypeAnnotator { /** * Constructor matching super. @@ -134,22 +139,33 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { super(atypeFactory); } + public boolean isResourceCollection(AnnotatedTypeMirror t) { + boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); + boolean isArrayType = t.getKind() == TypeKind.ARRAY; + AnnotatedTypeMirror componentType = + isArrayType + ? ((AnnotatedArrayType) t).getComponentType() + : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); + + MustCallAnnotatedTypeFactory mcAtf = + ResourceLeakUtils.getMustCallAnnotatedTypeFactory( + CollectionOwnershipAnnotatedTypeFactory.this); + + if (componentType != null) { + List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); + return list != null && list.size() > 0; + } else { + return false; + } + } + @Override public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { AnnotatedTypeMirror returnType = t.getReturnType(); - // check whether return type is resource collection - - boolean returnTypeIsCollection = - ResourceLeakUtils.isCollection(returnType.getUnderlyingType()); - // boolean returnTypeIsArray = - // returnType.getKind() == TypeKind.ARRAY; - if (returnTypeIsCollection) { - // System.out.println(t.getClass()); + + if (isResourceCollection(returnType)) { + returnType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); } - // AnnotatedTypeMirror componentType = - // returnTypeIsArray ? ((AnnotatedArrayType) returnType).getComponentType() - // : (returnTypeIsCollection ? ) - // ; return super.visitExecutable(t, p); } From 37e549e8146bfcabe1c4a6a10273fffa2bee44f1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 12:43:28 +0200 Subject: [PATCH 083/374] default resource collection return types to OwningCollection --- ...llectionOwnershipAnnotatedTypeFactory.java | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 524b75cc8fab..ad5fba9b1c7c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -4,12 +4,15 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeKind; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.MustCallInference; @@ -24,6 +27,8 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; @@ -123,7 +128,7 @@ protected TypeAnnotator createTypeAnnotator() { super.createTypeAnnotator(), new CollectionOwnershipTypeAnnotator(this)); } - private static class CollectionOwnershipTypeAnnotator extends TypeAnnotator { + private class CollectionOwnershipTypeAnnotator extends TypeAnnotator { /** * Constructor matching super. @@ -134,22 +139,47 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { super(atypeFactory); } + /** + * Returns whether the given type is a resource collection. + * + *

That is, whether the given type is: + * + *

    + *
  1. An array type, whose component has non-empty MustCall type. + *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall + * type. + *
+ * + * @param t the AnnotatedTypeMirror + * @return whether t is a resource collection + */ + public boolean isResourceCollection(AnnotatedTypeMirror t) { + boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); + boolean isArrayType = t.getKind() == TypeKind.ARRAY; + AnnotatedTypeMirror componentType = + isArrayType + ? ((AnnotatedArrayType) t).getComponentType() + : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); + + MustCallAnnotatedTypeFactory mcAtf = + ResourceLeakUtils.getMustCallAnnotatedTypeFactory( + CollectionOwnershipAnnotatedTypeFactory.this); + + if (componentType != null) { + List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); + return list != null && list.size() > 0; + } else { + return false; + } + } + @Override public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { AnnotatedTypeMirror returnType = t.getReturnType(); - // check whether return type is resource collection - - boolean returnTypeIsCollection = - ResourceLeakUtils.isCollection(returnType.getUnderlyingType()); - // boolean returnTypeIsArray = - // returnType.getKind() == TypeKind.ARRAY; - if (returnTypeIsCollection) { - // System.out.println(t.getClass()); + + if (isResourceCollection(returnType)) { + returnType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); } - // AnnotatedTypeMirror componentType = - // returnTypeIsArray ? ((AnnotatedArrayType) returnType).getComponentType() - // : (returnTypeIsCollection ? ) - // ; return super.visitExecutable(t, p); } From 2459bc6e335456367c8d4b711dc728aa85f41ad1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 14:48:16 +0200 Subject: [PATCH 084/374] more defaults for collectionownership type system --- ...llectionOwnershipAnnotatedTypeFactory.java | 203 ++++++++++-------- 1 file changed, 108 insertions(+), 95 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ad5fba9b1c7c..a6c7ca13308c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -1,12 +1,19 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; 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.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; @@ -29,9 +36,12 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the Collection Ownership Checker. */ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -122,12 +132,51 @@ public void postAnalyze(ControlFlowGraph cfg) { super.postAnalyze(cfg); } + /** + * Returns whether the given type is a resource collection. + * + *

That is, whether the given type is: + * + *

    + *
  1. An array type, whose component has non-empty MustCall type. + *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall + * type. + *
+ * + * @param t the AnnotatedTypeMirror + * @return whether t is a resource collection + */ + public boolean isResourceCollection(AnnotatedTypeMirror t) { + boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); + boolean isArrayType = t.getKind() == TypeKind.ARRAY; + AnnotatedTypeMirror componentType = + isArrayType + ? ((AnnotatedArrayType) t).getComponentType() + : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); + + MustCallAnnotatedTypeFactory mcAtf = + ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); + + if (componentType != null) { + List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); + return list != null && list.size() > 0; + } else { + return false; + } + } + @Override protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator( super.createTypeAnnotator(), new CollectionOwnershipTypeAnnotator(this)); } + @Override + public TreeAnnotator createTreeAnnotator() { + return new ListTreeAnnotator( + super.createTreeAnnotator(), new CollectionOwnershipTreeAnnotator(this)); + } + private class CollectionOwnershipTypeAnnotator extends TypeAnnotator { /** @@ -139,39 +188,6 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { super(atypeFactory); } - /** - * Returns whether the given type is a resource collection. - * - *

That is, whether the given type is: - * - *

    - *
  1. An array type, whose component has non-empty MustCall type. - *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall - * type. - *
- * - * @param t the AnnotatedTypeMirror - * @return whether t is a resource collection - */ - public boolean isResourceCollection(AnnotatedTypeMirror t) { - boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); - boolean isArrayType = t.getKind() == TypeKind.ARRAY; - AnnotatedTypeMirror componentType = - isArrayType - ? ((AnnotatedArrayType) t).getComponentType() - : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); - - MustCallAnnotatedTypeFactory mcAtf = - ResourceLeakUtils.getMustCallAnnotatedTypeFactory( - CollectionOwnershipAnnotatedTypeFactory.this); - - if (componentType != null) { - List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); - return list != null && list.size() > 0; - } else { - return false; - } - } @Override public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { @@ -181,23 +197,69 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void returnType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); } + for (AnnotatedTypeMirror paramType : t.getParameterTypes()) { + if (isResourceCollection(paramType)) { + boolean hasManualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP) != null; + if (!hasManualAnno) { + paramType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.NOTOWNINGCOLLECTION); + } + } + } + return super.visitExecutable(t, p); } } - // /* - // * The bulk of adding computed type annotations happens in the other overload - // * addComputedTypeAnnotations(Element, AnnotatedTypeMirror). - // * Here, we change the return type of methods annotated CollectionAlias to - // MustCallOnElementsUnknown, - // * such that at call-site, the returned alias will be guarded by the proper restrictions. - // * - // * Also the type of fields when they are accessed. - // */ - // @Override - // public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { - // super.addComputedTypeAnnotations(tree, type, useFlow); - // } + /* + * Change the default @MustCallOnElements type value of @OwningCollection fields and @OwningCollection + * method parameters to contain the @MustCall methods of the component, if no manual annotation is + * present. For example the type of: + * + * final @OwningCollection Socket[] s; + * + * is changed to @MustCallOnElements("close"). + */ + @Override + public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { + super.addComputedTypeAnnotations(elt, type); + + if (elt instanceof VariableElement) { + boolean isField = elt.getKind() == ElementKind.FIELD; + + if (isResourceCollection(type) && isField) { + AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); + if (fieldAnno == null || fieldAnno != BOTTOM) { + type.replaceAnnotation(OWNINGCOLLECTION); + } + } + } + } + + /** + * The TreeAnnotator for the Collection Ownership type system. + * + *

This tree annotator treats newly allocated resource arrays (arrays, whose component type has non-empty + * MustCall value). + */ + private class CollectionOwnershipTreeAnnotator extends TreeAnnotator { + + /** + * Create a CollectionOwnershipTreeAnnotator. + * + * @param collectionOwnershipAtf the type factory + */ + public CollectionOwnershipTreeAnnotator(CollectionOwnershipAnnotatedTypeFactory collectionOwnershipAtf) { + super(collectionOwnershipAtf); + } + + @Override + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + if (isResourceCollection(type)) { + type.replaceAnnotation(OWNINGCOLLECTION); + } + return super.visitNewArray(tree, type); + } + } } // @Override @@ -205,52 +267,3 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void // return new MustCallQualifierPolymorphism(processingEnv, this); // } - // /** - // * The TreeAnnotator for the MustCall type system. - // * - // *

This tree annotator treats non-owning method parameters as bottom, regardless of their - // * declared type, when they appear in the body of the method. Doing so is safe because being - // * non-owning means, by definition, that their must-call obligations are only relevant in the - // * callee. (This behavior is disabled if the {@code -AnoLightweightOwnership} option is - // passed - // to - // * the checker.) - // * - // *

The tree annotator also changes the type of resource variables to remove "close" from - // their - // * must-call types, because the try-with-resources statement guarantees that close() is - // called - // on - // * all such variables. - // */ - // private class MustCallTreeAnnotator extends TreeAnnotator { - // /** - // * Create a MustCallTreeAnnotator. - // * - // * @param mustCallAnnotatedTypeFactory the type factory - // */ - // public MustCallTreeAnnotator(MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory) { - // super(mustCallAnnotatedTypeFactory); - // } - - // @Override - // public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror type) { - // Element elt = TreeUtils.elementFromUse(tree); - // // The following changes are not desired for RLC _inference_ in unannotated programs, - // // where a goal is to infer and add @Owning annotations to formal parameters. - // // Therefore, if WPI is enabled, they should not be executed. - // if (getWholeProgramInference() == null - // && elt.getKind() == ElementKind.PARAMETER - // && (noLightweightOwnership || getDeclAnnotation(elt, Owning.class) == null)) { - // if (!type.hasPrimaryAnnotation(POLY)) { - // // Parameters that are not annotated with @Owning should be treated as bottom - // // (to suppress warnings about them). An exception is polymorphic parameters, - // // which might be @MustCallAlias (and so wouldn't be annotated with @Owning): - // // these are not modified, to support verification of @MustCallAlias - // // annotations. - // type.replaceAnnotation(BOTTOM); - // } - // } - // return super.visitIdentifier(tree, type); - // } - // } From 196a8549a9360cfdaea5e08b61b0e8355dde1e76 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 14:52:06 +0200 Subject: [PATCH 085/374] more defaulting --- ...llectionOwnershipAnnotatedTypeFactory.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 91919b068f26..5b803e5d2872 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -2,7 +2,6 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.LinkedHashSet; @@ -11,8 +10,6 @@ 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.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; @@ -37,11 +34,10 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the Collection Ownership Checker. */ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -154,8 +150,7 @@ public boolean isResourceCollection(AnnotatedTypeMirror t) { ? ((AnnotatedArrayType) t).getComponentType() : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); - MustCallAnnotatedTypeFactory mcAtf = - ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); + MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); if (componentType != null) { List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); @@ -200,7 +195,8 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void if (isResourceCollection(paramType)) { boolean hasManualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP) != null; if (!hasManualAnno) { - paramType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.NOTOWNINGCOLLECTION); + paramType.replaceAnnotation( + CollectionOwnershipAnnotatedTypeFactory.this.NOTOWNINGCOLLECTION); } } } @@ -237,8 +233,8 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { /** * The TreeAnnotator for the Collection Ownership type system. * - *

This tree annotator treats newly allocated resource arrays (arrays, whose component type has non-empty - * MustCall value). + *

This tree annotator treats newly allocated resource arrays (arrays, whose component type has + * non-empty MustCall value). */ private class CollectionOwnershipTreeAnnotator extends TreeAnnotator { @@ -247,7 +243,8 @@ private class CollectionOwnershipTreeAnnotator extends TreeAnnotator { * * @param collectionOwnershipAtf the type factory */ - public CollectionOwnershipTreeAnnotator(CollectionOwnershipAnnotatedTypeFactory collectionOwnershipAtf) { + public CollectionOwnershipTreeAnnotator( + CollectionOwnershipAnnotatedTypeFactory collectionOwnershipAtf) { super(collectionOwnershipAtf); } @@ -265,4 +262,3 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { // protected QualifierPolymorphism createQualifierPolymorphism() { // return new MustCallQualifierPolymorphism(processingEnv, this); // } - From 0e7fd507ed3fdc8d03234d6bce53017722d6880a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 15:08:58 +0200 Subject: [PATCH 086/374] remove expected errors after changing type var upper bound for collections --- checker/tests/mustcall/ListOfMustCall.java | 4 ---- checker/tests/resourceleak/EnhancedFor.java | 1 - checker/tests/resourceleak/SocketIntoList.java | 1 - 3 files changed, 6 deletions(-) diff --git a/checker/tests/mustcall/ListOfMustCall.java b/checker/tests/mustcall/ListOfMustCall.java index 3d6648ee20ce..21c0d5d2ca8f 100644 --- a/checker/tests/mustcall/ListOfMustCall.java +++ b/checker/tests/mustcall/ListOfMustCall.java @@ -12,8 +12,6 @@ @InheritableMustCall("a") class ListOfMustCall { static void test(ListOfMustCall lm) { - // :: error: type.argument - // :: error: type.arguments.not.inferred List l = new ArrayList<>(); // add(E e) takes an object of the type argument's type l.add(lm); @@ -22,8 +20,6 @@ static void test(ListOfMustCall lm) { } static void test2(ListOfMustCall lm) { - // :: error: type.argument - // :: error: type.arguments.not.inferred List<@MustCall("a") ListOfMustCall> l = new ArrayList<>(); l.add(lm); l.remove(lm); diff --git a/checker/tests/resourceleak/EnhancedFor.java b/checker/tests/resourceleak/EnhancedFor.java index 2f2afcac14b7..92f61b8f774b 100644 --- a/checker/tests/resourceleak/EnhancedFor.java +++ b/checker/tests/resourceleak/EnhancedFor.java @@ -25,7 +25,6 @@ void test2(List<@MustCall Socket> list) { } } - // :: error: type.argument void test3(List list) { // This error is issued because `s` is a local variable, and // the foreach loop under the hood assigns the result of a call diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index d95c7a33c638..1f44f7b7bfdb 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -13,7 +13,6 @@ public void test1(List<@MustCall({}) Socket> l) { l.add(s); } - // :: error: type.argument public void test2(List l) { // s is unconnected, so no error is expected when it's stored into the list. // But, if the list is unannotated, we do get an error at its declaration site From 0a1a271663bd6343b98104e8e3bb625c892dad00 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 15:09:28 +0200 Subject: [PATCH 087/374] make locals default to top for collectionownership type system --- .../collectionownership/qual/OwningCollectionBottom.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index 22517e63185a..5cd6121bf855 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -28,7 +28,7 @@ // TypeUseLocation.FIELD, // TypeUseLocation.IMPLICIT_LOWER_BOUND, // TypeUseLocation.IMPLICIT_UPPER_BOUND, - TypeUseLocation.LOCAL_VARIABLE, + // TypeUseLocation.LOCAL_VARIABLE, // TypeUseLocation.LOWER_BOUND, // TypeUseLocation.OTHERWISE, // TypeUseLocation.PARAMETER, From 3e2ff247946146b29b2306211d31b4f9ef4bd149 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 25 May 2025 23:54:33 +0200 Subject: [PATCH 088/374] fix defaults --- ...llectionOwnershipAnnotatedTypeFactory.java | 37 +++++++++++++------ .../CollectionOwnershipDefaults.java | 26 +++++++------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 5b803e5d2872..15b3fc90d002 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -172,6 +172,12 @@ public TreeAnnotator createTreeAnnotator() { super.createTreeAnnotator(), new CollectionOwnershipTreeAnnotator(this)); } + /** + * The TypeAnnotator for the Collection Ownership type system. + * + *

This TypeAnnotator defaults resource collection return types to OwningCollection, and + * resource collection parameters to NotOwningCollection. + */ private class CollectionOwnershipTypeAnnotator extends TypeAnnotator { /** @@ -206,13 +212,11 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void } /* - * Change the default @MustCallOnElements type value of @OwningCollection fields and @OwningCollection - * method parameters to contain the @MustCall methods of the component, if no manual annotation is - * present. For example the type of: - * - * final @OwningCollection Socket[] s; + * Default resource collection fields to @OwningCollection and resource collection parameters to + * @NotOwningCollection (inside the method). * - * is changed to @MustCallOnElements("close"). + * Resource collections are either java.util.Collection's or arrays, whose component has + * non-empty @MustCall type, as defined by the predicate isResourceCollection(AnnotatedTypeMirror). */ @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { @@ -220,11 +224,20 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { if (elt instanceof VariableElement) { boolean isField = elt.getKind() == ElementKind.FIELD; - - if (isResourceCollection(type) && isField) { - AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); - if (fieldAnno == null || fieldAnno != BOTTOM) { - type.replaceAnnotation(OWNINGCOLLECTION); + boolean isParam = elt.getKind() == ElementKind.PARAMETER; + boolean isResourceCollection = isResourceCollection(type); + + if (isResourceCollection) { + if (isField) { + AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); + if (fieldAnno == null || fieldAnno == BOTTOM) { + type.replaceAnnotation(OWNINGCOLLECTION); + } + } else if (isParam) { + AnnotationMirror paramAnno = type.getEffectiveAnnotationInHierarchy(TOP); + if (paramAnno == null || paramAnno == BOTTOM) { + type.replaceAnnotation(NOTOWNINGCOLLECTION); + } } } } @@ -234,7 +247,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { * The TreeAnnotator for the Collection Ownership type system. * *

This tree annotator treats newly allocated resource arrays (arrays, whose component type has - * non-empty MustCall value). + * non-empty MustCall value) as @OwningCollection. */ private class CollectionOwnershipTreeAnnotator extends TreeAnnotator { diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index c6e8037ff91a..114f84ca39d8 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -27,17 +27,19 @@ void checkResourceCollectionFieldDefault() { } /* - * This method checks that its parameter defaults to @NotOwningCollection. - - * Parameter should default to @NotOwningCollection. - * Return type should default to @OwningCollection. - */ - List identity(List list) { + * This method checks that its parameter defaults to @NotOwningCollection. + */ + void checkParamIsNotOwningCollection(List list) { // list : @NotOwningCollection. Thus, next line should throw an error. // :: error: argument checkArgIsOwning(list); + } + /* + * Return type should default to @OwningCollection. + */ + List identity(@OwningCollection List list) { return list; } @@ -66,10 +68,10 @@ void checkResourceCollectionReturnValueDefault() { * Checks that a newly allocated resource collection has type @OwningCollection. */ void checkNewResourceCollectionDefault() { - List newResourceCollection = new ArrayList(); - checkArgIsOwning(newResourceCollection); - // :: error: argument - checkArgIsOCwoO(newResourceCollection); + // List newResourceCollection = new ArrayList(); + // checkArgIsOwning(newResourceCollection); + // // :: error: argument + // checkArgIsOCwoO(newResourceCollection); Socket[] newResourceArray = new Socket[n]; checkArgIsOwning(newResourceArray); @@ -79,9 +81,9 @@ void checkNewResourceCollectionDefault() { void checkArgIsOwning(@OwningCollection Collection collection) {} - void checkArgIsOwning(@OwningCollection Socket[] collection) {} + void checkArgIsOwning(Socket @OwningCollection [] collection) {} void checkArgIsOCwoO(@OwningCollectionWithoutObligation Collection collection) {} - void checkArgIsOCwoO(@OwningCollectionWithoutObligation Socket[] collection) {} + void checkArgIsOCwoO(Socket @OwningCollectionWithoutObligation [] collection) {} } From 0fff489a8e21824412ea9ed73b5fd8097e7ad4ba Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 29 May 2025 20:33:26 +0200 Subject: [PATCH 089/374] use areSameByName to compare annotations --- .../CollectionOwnershipAnnotatedTypeFactory.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 15b3fc90d002..3fdb1696422c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -38,6 +38,7 @@ import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; /** The annotated type factory for the Collection Ownership Checker. */ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -199,8 +200,8 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void for (AnnotatedTypeMirror paramType : t.getParameterTypes()) { if (isResourceCollection(paramType)) { - boolean hasManualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP) != null; - if (!hasManualAnno) { + AnnotationMirror manualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP); + if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { paramType.replaceAnnotation( CollectionOwnershipAnnotatedTypeFactory.this.NOTOWNINGCOLLECTION); } @@ -230,12 +231,12 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { if (isResourceCollection) { if (isField) { AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); - if (fieldAnno == null || fieldAnno == BOTTOM) { + if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { type.replaceAnnotation(OWNINGCOLLECTION); } } else if (isParam) { AnnotationMirror paramAnno = type.getEffectiveAnnotationInHierarchy(TOP); - if (paramAnno == null || paramAnno == BOTTOM) { + if (paramAnno == null || AnnotationUtils.areSameByName(BOTTOM, paramAnno)) { type.replaceAnnotation(NOTOWNINGCOLLECTION); } } From 37d6e16fee19d5681acfe57d4b6042ecbcc497f7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 29 May 2025 21:07:05 +0200 Subject: [PATCH 090/374] ensure wildcards/generics have MustCall() type vars --- .../MustCallAnnotatedTypeFactory.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index a2ab9bcbddf4..69ab999b509d 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -23,6 +23,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; @@ -33,6 +34,7 @@ import org.checkerframework.checker.mustcall.qual.PolyMustCall; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.block.Block; @@ -42,6 +44,7 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.QualifierHierarchy; @@ -157,6 +160,76 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { tempVars.clear(); } + /** + * Called in addComputedTypeAnnotations. Changes the type parameters referring to collections and + * iterators to {@code @MustCall} if they are currently {@code @MustCallUnknown}. + * + *

This is necessary, as the type variable upper bounds for collections is + * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, + * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and + * imprecise. + * + * @param elt the element + * @param type the type of the element + */ + private void changeCollectionTypeParameters(Element elt, AnnotatedTypeMirror type) { + if (elt.getKind() != ElementKind.CLASS && elt.getKind() != ElementKind.INTERFACE) { + if (type.getKind() == TypeKind.DECLARED) { + replaceResourceHoldingTypeVarsWithBottomIfTop(type); + } else if (type.getKind() == TypeKind.EXECUTABLE) { + AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; + AnnotatedTypeMirror returnType = methodType.getReturnType(); + + replaceResourceHoldingTypeVarsWithBottomIfTop(returnType); + + String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); + if (enclosingClass.startsWith("java.")) { + // this is a jdk method - do not change the upper bound + } else { + for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { + replaceResourceHoldingTypeVarsWithBottomIfTop(paramType); + } + } + } + } + } + + /** + * Replace all the type variables of the given AnnotatedTypeMirror that refer to Collections or + * Iterators with Bottom if they are Top. This is because having a Top type parameter is unsafe + * and occurs when the type variable is a generic or wildcard without upper bound. We want to + * prevent such a Collection/Iterator from holding elements with MustCall obligations. + * + * @param typeMirror the annotated type mirror for which all Collection/Iterator type variables + * with Top type are to be replaced with Bottom. + */ + private void replaceResourceHoldingTypeVarsWithBottomIfTop(AnnotatedTypeMirror typeMirror) { + if (ResourceLeakUtils.isCollection(typeMirror.getUnderlyingType())) { + // || ResourceLeakUtils.isIterator(typeMirror.getUnderlyingType())) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + AnnotatedDeclaredType adt = (AnnotatedDeclaredType) typeMirror; + for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) { + if (typeArg == null) continue; + AnnotationMirror mcAnno = typeArg.getEffectiveAnnotation(); + boolean typeArgIsMcoeUnknown = + mcAnno != null + && processingEnv + .getTypeUtils() + .isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType()); + if (typeArgIsMcoeUnknown) { + typeArg.replaceAnnotation(BOTTOM); + } + } + } + } + } + + @Override + public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { + super.addComputedTypeAnnotations(elt, type); + changeCollectionTypeParameters(elt, type); + } + @Override protected Set> createSupportedTypeQualifiers() { // Explicitly name the qualifiers, in order to exclude @MustCallAlias. From 1dff59feab45cda01918b14c3b9d197c39f9ce5e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 29 May 2025 22:45:09 +0200 Subject: [PATCH 091/374] refactor call to change mustcall type of typevars --- .../MustCallAnnotatedTypeFactory.java | 63 +++++++++---------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 69ab999b509d..1d202a247cf5 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -160,40 +160,6 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { tempVars.clear(); } - /** - * Called in addComputedTypeAnnotations. Changes the type parameters referring to collections and - * iterators to {@code @MustCall} if they are currently {@code @MustCallUnknown}. - * - *

This is necessary, as the type variable upper bounds for collections is - * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, - * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and - * imprecise. - * - * @param elt the element - * @param type the type of the element - */ - private void changeCollectionTypeParameters(Element elt, AnnotatedTypeMirror type) { - if (elt.getKind() != ElementKind.CLASS && elt.getKind() != ElementKind.INTERFACE) { - if (type.getKind() == TypeKind.DECLARED) { - replaceResourceHoldingTypeVarsWithBottomIfTop(type); - } else if (type.getKind() == TypeKind.EXECUTABLE) { - AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; - AnnotatedTypeMirror returnType = methodType.getReturnType(); - - replaceResourceHoldingTypeVarsWithBottomIfTop(returnType); - - String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); - if (enclosingClass.startsWith("java.")) { - // this is a jdk method - do not change the upper bound - } else { - for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { - replaceResourceHoldingTypeVarsWithBottomIfTop(paramType); - } - } - } - } - } - /** * Replace all the type variables of the given AnnotatedTypeMirror that refer to Collections or * Iterators with Bottom if they are Top. This is because having a Top type parameter is unsafe @@ -224,10 +190,37 @@ private void replaceResourceHoldingTypeVarsWithBottomIfTop(AnnotatedTypeMirror t } } + /* + * Changes the type parameters of collections and iteratos to @MustCall if they are currently + * @MustCallUnknown. + * + *

This is necessary, as the type variable upper bounds for collections is + * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, + * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and + * imprecise. + */ @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { super.addComputedTypeAnnotations(elt, type); - changeCollectionTypeParameters(elt, type); + if (elt.getKind() != ElementKind.CLASS && elt.getKind() != ElementKind.INTERFACE) { + if (type.getKind() == TypeKind.DECLARED) { + replaceResourceHoldingTypeVarsWithBottomIfTop(type); + } else if (type.getKind() == TypeKind.EXECUTABLE) { + AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; + AnnotatedTypeMirror returnType = methodType.getReturnType(); + + replaceResourceHoldingTypeVarsWithBottomIfTop(returnType); + + String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); + if (enclosingClass.startsWith("java.")) { + // this is a jdk method - do not change the upper bound + } else { + for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { + replaceResourceHoldingTypeVarsWithBottomIfTop(paramType); + } + } + } + } } @Override From d44a95eb96832bb97bde2bdadfe5adebeead18c4 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 30 May 2025 01:04:42 +0200 Subject: [PATCH 092/374] change type var of nested collections as well --- .../MustCallAnnotatedTypeFactory.java | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 1d202a247cf5..e3bb46092498 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -166,25 +166,34 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { * and occurs when the type variable is a generic or wildcard without upper bound. We want to * prevent such a Collection/Iterator from holding elements with MustCall obligations. * - * @param typeMirror the annotated type mirror for which all Collection/Iterator type variables - * with Top type are to be replaced with Bottom. + * @param tree the tree + * @param adt the annotated declared type for which all Collection type variables with Top type + * are to be replaced with Bottom. */ - private void replaceResourceHoldingTypeVarsWithBottomIfTop(AnnotatedTypeMirror typeMirror) { - if (ResourceLeakUtils.isCollection(typeMirror.getUnderlyingType())) { - // || ResourceLeakUtils.isIterator(typeMirror.getUnderlyingType())) { - if (typeMirror.getKind() == TypeKind.DECLARED) { - AnnotatedDeclaredType adt = (AnnotatedDeclaredType) typeMirror; - for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) { - if (typeArg == null) continue; + private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclaredType adt) { + if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) { + // || ResourceLeakUtils.isIterator(adt.getUnderlyingType())) { + for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) { + if (typeArg == null) continue; + if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) { + if (tree != null && tree instanceof NewClassTree) { + if (((NewClassTree) tree).getTypeArguments().isEmpty()) { + // Diamond [new Class()<>]. Not explicit generic type param. + // This will be inferred later. Don't put it to bottom here. + continue; + } + } AnnotationMirror mcAnno = typeArg.getEffectiveAnnotation(); - boolean typeArgIsMcoeUnknown = + boolean typeArgIsMcUnknown = mcAnno != null && processingEnv .getTypeUtils() .isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType()); - if (typeArgIsMcoeUnknown) { + if (typeArgIsMcUnknown) { typeArg.replaceAnnotation(BOTTOM); } + } else if (typeArg.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg); } } } @@ -200,23 +209,31 @@ private void replaceResourceHoldingTypeVarsWithBottomIfTop(AnnotatedTypeMirror t * imprecise. */ @Override - public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { - super.addComputedTypeAnnotations(elt, type); - if (elt.getKind() != ElementKind.CLASS && elt.getKind() != ElementKind.INTERFACE) { + public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { + super.addComputedTypeAnnotations(tree, type, useFlow); + + if (tree.getKind() != Tree.Kind.CLASS && tree.getKind() != Tree.Kind.INTERFACE) { if (type.getKind() == TypeKind.DECLARED) { - replaceResourceHoldingTypeVarsWithBottomIfTop(type); + replaceCollectionTypeVarsWithBottomIfTop(tree, (AnnotatedDeclaredType) type); } else if (type.getKind() == TypeKind.EXECUTABLE) { AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; AnnotatedTypeMirror returnType = methodType.getReturnType(); - replaceResourceHoldingTypeVarsWithBottomIfTop(returnType); + if (returnType.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) returnType); + } - String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); - if (enclosingClass.startsWith("java.")) { - // this is a jdk method - do not change the upper bound - } else { - for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { - replaceResourceHoldingTypeVarsWithBottomIfTop(paramType); + Element elt = TreeUtils.elementFromTree(tree); + if (elt != null && elt instanceof ExecutableElement) { + String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); + if (enclosingClass.startsWith("java.")) { + // this is a jdk method - do not change the upper bound + } else { + for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { + if (paramType.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) paramType); + } + } } } } From cf0b5d000af2ab8122a713a88d25cbb37a4e541a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 01:21:46 +0200 Subject: [PATCH 093/374] check component type for manual anno when deciding resource collection --- .../CollectionOwnershipAnnotatedTypeFactory.java | 16 ++++++++++------ .../checker/resourceleak/ResourceLeakUtils.java | 7 +++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 3fdb1696422c..313bb498a50a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -11,7 +11,10 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; @@ -31,8 +34,6 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; @@ -146,15 +147,18 @@ public void postAnalyze(ControlFlowGraph cfg) { public boolean isResourceCollection(AnnotatedTypeMirror t) { boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); boolean isArrayType = t.getKind() == TypeKind.ARRAY; - AnnotatedTypeMirror componentType = + + TypeMirror componentType = isArrayType - ? ((AnnotatedArrayType) t).getComponentType() - : (isCollectionType ? ((AnnotatedDeclaredType) t).getTypeArguments().get(0) : null); + ? ((ArrayType) t.getUnderlyingType()).getComponentType() + : (isCollectionType + ? ((DeclaredType) t.getUnderlyingType()).getTypeArguments().get(0) + : null); MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); if (componentType != null) { - List list = ResourceLeakUtils.getMcValues(componentType.getUnderlyingType(), mcAtf); + List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); return list != null && list.size() > 0; } else { return false; diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 8188aa423175..fc441a643ae4 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -287,6 +287,13 @@ public static boolean isCollection(TypeMirror type) { return null; } } + + AnnotationMirror manualAnno = + AnnotationUtils.getAnnotationByClass(type.getAnnotationMirrors(), MustCall.class); + if (manualAnno != null) { + return AnnotationUtils.getElementValueArray( + manualAnno, mcAtf.getMustCallValueElement(), String.class); + } TypeElement typeElement = TypesUtils.getTypeElement(type); AnnotationMirror imcAnnotation = mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); From 5bc492f6143514bff194ab7fcb6b159ca6777ac4 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 16:30:49 +0200 Subject: [PATCH 094/374] handle annotationvalue null for MustCall() annotation --- ...llectionOwnershipAnnotatedTypeFactory.java | 1 - .../resourceleak/ResourceLeakUtils.java | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 313bb498a50a..f4b66783df44 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -118,7 +118,6 @@ public void postAnalyze(ControlFlowGraph cfg) { rlc.setRoot(root); MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); mustCallConsistencyAnalyzer.analyze(cfg); - // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. if (cmAtf.getWholeProgramInference() != null) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index fc441a643ae4..9c32960b49d1 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,11 +3,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; @@ -25,6 +25,7 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; @@ -260,12 +261,34 @@ public static boolean isCollection(TypeMirror type) { return Collection.class.isAssignableFrom(elementRawType); } + /** + * Safely extract the values of a given element in an {@link AnnotationMirror}. Returns the list + * of such values and the empty list if the element is not present. + * + * @param anno the annotation to extract values from + * @param element the element to access from the annotation + * @return the list of values of the given element in the given annotation and the empty list if + * it is not present + */ + public static List getValuesInAnno(AnnotationMirror anno, ExecutableElement element) { + if (anno == null) { + throw new BugInCF("Annotation " + anno + " must not be null."); + } else { + AnnotationValue av = anno.getElementValues().get(element); + if (av == null) { + return new ArrayList(); + } else { + return AnnotationUtils.annotationValueToList(av, String.class); + } + } + } + /** * Returns the list of mustcall obligations for the given {@code TypeMirror} upper bound (either * the type variable itself if it is concrete or the upper bound if its a wildcard or generic). * *

If the type variable has no upper bound, for instance if it is a wildcard with no extends - * clause the method returns null + * clause the method returns null. * * @param type the {@code TypeMirror} * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type @@ -291,24 +314,19 @@ public static boolean isCollection(TypeMirror type) { AnnotationMirror manualAnno = AnnotationUtils.getAnnotationByClass(type.getAnnotationMirrors(), MustCall.class); if (manualAnno != null) { - return AnnotationUtils.getElementValueArray( - manualAnno, mcAtf.getMustCallValueElement(), String.class); + return getValuesInAnno(manualAnno, mcAtf.getMustCallValueElement()); } + TypeElement typeElement = TypesUtils.getTypeElement(type); AnnotationMirror imcAnnotation = mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); - Set mcValues = new HashSet<>(); if (mcAnnotation != null) { - mcValues.addAll( - AnnotationUtils.getElementValueArray( - mcAnnotation, mcAtf.getMustCallValueElement(), String.class)); + return getValuesInAnno(mcAnnotation, mcAtf.getMustCallValueElement()); } if (imcAnnotation != null) { - mcValues.addAll( - AnnotationUtils.getElementValueArray( - imcAnnotation, mcAtf.getInheritableMustCallValueElement(), String.class)); + return getValuesInAnno(imcAnnotation, mcAtf.getInheritableMustCallValueElement()); } - return new ArrayList<>(mcValues); + return new ArrayList<>(); } } From aff8a80c1f2eedce84cae4803186b9af599bebd4 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 16:31:30 +0200 Subject: [PATCH 095/374] move owningoverride error codes to rlc messages where they are actually issued --- .../checkerframework/checker/resourceleak/messages.properties | 2 -- .../checker/rlccalledmethods/messages.properties | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index a9df3c247117..8f2cbaa5d7ee 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -7,6 +7,4 @@ creates.mustcall.for.invalid.target=Cannot create a must-call obligation for "%s destructor.exceptional.postcondition=Method %s must resolve the must-call obligations of the owning field %s along all paths, including exceptional paths. On an exceptional path, the @EnsuresCalledMethods annotation was not satisfied.%nFound: %s%nRequired: %s mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope without being assigned into an owning field of this object (if this is a constructor) or returned.%nReason for going out of scope: %s mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in pairs (one on a return type and one on a parameter type).%nBut %s -owning.override.param=Incompatible ownership for parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s -owning.override.return=Incompatible ownership for return.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties new file mode 100644 index 000000000000..1935440e2db2 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties @@ -0,0 +1,2 @@ +owning.override.return=Incompatible ownership for return.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s +owning.override.param=Incompatible ownership for parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s From 2f66cf6b24cfecd7cbcf8c7fa9f5aa2bbe6216b1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 16:31:56 +0200 Subject: [PATCH 096/374] adapt expected test outcomes for type var upper bound elevation for collections and collection ownership checker --- checker/tests/resourceleak/EnhancedFor.java | 10 +++------- checker/tests/resourceleak/Issue6030.java | 7 ++++++- checker/tests/resourceleak/SocketIntoList.java | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/checker/tests/resourceleak/EnhancedFor.java b/checker/tests/resourceleak/EnhancedFor.java index 92f61b8f774b..56f251e1e43b 100644 --- a/checker/tests/resourceleak/EnhancedFor.java +++ b/checker/tests/resourceleak/EnhancedFor.java @@ -26,13 +26,9 @@ void test2(List<@MustCall Socket> list) { } void test3(List list) { - // This error is issued because `s` is a local variable, and - // the foreach loop under the hood assigns the result of a call - // to Iterator#next into it (which is owning by default, because it's - // a method return type). Both this error and the type.argument error - // above can be suppressed by writing @MustCall on the Socket type, as in - // test4 below (but note that this will make call sites difficult to verify). - // :: error: (required.method.not.called) + // With the extension of the RLC to collections, this is no longer an error. + // The return type of Iterator#next is NotOwning, because the iterator does not + // own the elements, but instead the host collection the iterator is associated with. for (Socket s : list) {} } diff --git a/checker/tests/resourceleak/Issue6030.java b/checker/tests/resourceleak/Issue6030.java index abb621a5cfdd..ecf3c5af5179 100644 --- a/checker/tests/resourceleak/Issue6030.java +++ b/checker/tests/resourceleak/Issue6030.java @@ -1,6 +1,7 @@ // Test case for https://github.com/typetools/checker-framework/issues/6030 import java.util.*; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; public class Issue6030 { @@ -16,7 +17,11 @@ public boolean hasNext() { return iterator.hasNext(); } - public T next() { + /* + * The @NotOwning annotation is required to be consistent with the superclass implementation. + * The return type of Iterator#next is @NotOwning. Soundness is ensured by the RLC for collections. + */ + public @NotOwning T next() { return null; } diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index 1f44f7b7bfdb..b68798f2fec4 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -4,6 +4,7 @@ import java.net.*; import java.util.List; +import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.mustcall.qual.*; public class SocketIntoList { @@ -13,7 +14,7 @@ public void test1(List<@MustCall({}) Socket> l) { l.add(s); } - public void test2(List l) { + public void test2(@OwningCollection List l) { // s is unconnected, so no error is expected when it's stored into the list. // But, if the list is unannotated, we do get an error at its declaration site // (as expected, due to #5912). From 0618b02641f641d592f22c45afb613150c807a4c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 19:06:47 +0200 Subject: [PATCH 097/374] prevent index out of bounds reading typeargs --- ...llectionOwnershipAnnotatedTypeFactory.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f4b66783df44..442d6ed53bf2 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -147,12 +147,16 @@ public boolean isResourceCollection(AnnotatedTypeMirror t) { boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); boolean isArrayType = t.getKind() == TypeKind.ARRAY; - TypeMirror componentType = - isArrayType - ? ((ArrayType) t.getUnderlyingType()).getComponentType() - : (isCollectionType - ? ((DeclaredType) t.getUnderlyingType()).getTypeArguments().get(0) - : null); + TypeMirror componentType = null; + if (isArrayType) { + componentType = ((ArrayType) t.getUnderlyingType()).getComponentType(); + } else if (isCollectionType) { + List typeArgs = + ((DeclaredType) t.getUnderlyingType()).getTypeArguments(); + if (typeArgs.size() != 0) { + componentType = typeArgs.get(0); + } + } MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); @@ -198,7 +202,11 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void AnnotatedTypeMirror returnType = t.getReturnType(); if (isResourceCollection(returnType)) { - returnType.replaceAnnotation(CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); + AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); + if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { + returnType.replaceAnnotation( + CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); + } } for (AnnotatedTypeMirror paramType : t.getParameterTypes()) { From e8ab6c1e05a9fdb51051f106d240a2b867b51bf1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 19:07:01 +0200 Subject: [PATCH 098/374] add nullness check for typeelement --- .../checker/resourceleak/ResourceLeakUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 9c32960b49d1..cce242a92013 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -318,6 +318,9 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem } TypeElement typeElement = TypesUtils.getTypeElement(type); + if (typeElement == null) { + return new ArrayList<>(); + } AnnotationMirror imcAnnotation = mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); From 83273db7974b5bf253f32ee20b72e834e4e9ffa8 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 1 Jun 2025 19:07:20 +0200 Subject: [PATCH 099/374] add tests for manual return type annotation for resource collections --- .../CollectionOwnershipDefaults.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index 114f84ca39d8..da1cdfb0bbe5 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -1,6 +1,7 @@ import java.net.Socket; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedList; import java.util.List; import org.checkerframework.checker.calledmethods.qual.*; import org.checkerframework.checker.collectionownership.qual.*; @@ -17,6 +18,31 @@ class CollectionOwnershipDefaults { /* This should default to @OwningCollection */ Collection resourceCollectionField; + /* + * Check that manual MustCall annotations are correctly considered when deciding whether + * something is a resoure collection. + */ + public void detectResourceCollection( + List<@MustCall({}) Socket> l1, + Collection<@MustCall({"close"}) Socket> l2, + LinkedList l3, + ArrayList l4, + ArrayList<@MustCall({"a"}) String> l5) { + // not a resource collection, this call succeeds. + checkArgIsOwning(l1); + // a resource collection, this call fails. + // :: error: argument + checkArgIsOwning(l2); + // a resource collection, this call fails. + // :: error: argument + checkArgIsOwning(l3); + // not a resource collection, this call succeeds. + checkArgIsOwning(l4); + // a resource collection, this call fails. + // :: error: argument + checkArgIsOwning(l5); + } + /* * Check that resource collection field defaults to @OwningCollection. */ @@ -43,6 +69,23 @@ List identity(@OwningCollection List list) { return list; } + /* + * Check whether manual annotations on the return type correctly override the default. + */ + @NotOwningCollection + List overrideReturnType(List list) { + return list; + } + + void overrideReturnTypeClient() { + List notOwninglist = overrideReturnType(new ArrayList()); + List owninglist = identity(new ArrayList()); + + checkArgIsOwning(owninglist); + // :: error: argument + checkArgIsOwning(notOwninglist); + } + /* * Checks that an @OwningCollection can be passed to a resource collection parameter (because it should default * to @OwningCollection). I.e. check that the resource collection parameter default is visible at call-site as well. @@ -79,11 +122,11 @@ void checkNewResourceCollectionDefault() { checkArgIsOCwoO(newResourceArray); } - void checkArgIsOwning(@OwningCollection Collection collection) {} + void checkArgIsOwning(@OwningCollection Collection collection) {} void checkArgIsOwning(Socket @OwningCollection [] collection) {} - void checkArgIsOCwoO(@OwningCollectionWithoutObligation Collection collection) {} + void checkArgIsOCwoO(@OwningCollectionWithoutObligation Collection collection) {} void checkArgIsOCwoO(Socket @OwningCollectionWithoutObligation [] collection) {} } From 5a7db0d5d5e6aeb576ffd1fbbbba9559f07afffd Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 2 Jun 2025 14:15:18 +0200 Subject: [PATCH 100/374] helper method to get manual MustCallUnknown anno from typemirror --- .../checker/resourceleak/ResourceLeakUtils.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index cce242a92013..91999bb60fb8 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -19,6 +19,7 @@ import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallUnknown; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; @@ -332,4 +333,20 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem } return new ArrayList<>(); } + + /** + * Return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation. + * + * @param typeMirror the {@code TypeMirror} + * @return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation + */ + public static boolean hasManualMustCallUnknownAnno(TypeMirror typeMirror) { + if (typeMirror == null) return false; + for (AnnotationMirror paramAnno : typeMirror.getAnnotationMirrors()) { + if (AnnotationUtils.areSameByName(paramAnno, MustCallUnknown.class.getCanonicalName())) { + return true; + } + } + return false; + } } From f536855d33e774b187bc60c99b694221d4365b20 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 2 Jun 2025 14:15:36 +0200 Subject: [PATCH 101/374] warning suppresion in unsafe call to plumelib --- .../checkerframework/common/value/ValueCheckerUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java index 2ec5c5f99a2f..1063b0a916d1 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java @@ -173,11 +173,17 @@ private static T convertLongToType(long value, Class expectedType) { * @param origValues the objects to format * @return a list of the formatted objects */ + @SuppressWarnings("mustcall:type.arguments.not.inferred") private static @Nullable List convertToStringVal( List origValues) { if (origValues == null) { return null; } + // this line is unsafe, which is why the warning suppression is required. + // `origValues` potentially carries elements with MustCall obligations, + // but the second parameter of mapList is not annotated to do so. + // It would have to have the same List + // annotation. return CollectionsPlume.mapList(Object::toString, origValues); } From ebcb076dc0306e10e86291ef3193c960d4d2f7cb Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 2 Jun 2025 14:15:57 +0200 Subject: [PATCH 102/374] correct unsoundness in CO defaulting test --- .../CollectionOwnershipDefaults.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index da1cdfb0bbe5..b00f958238f5 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -122,11 +122,14 @@ void checkNewResourceCollectionDefault() { checkArgIsOCwoO(newResourceArray); } - void checkArgIsOwning(@OwningCollection Collection collection) {} + void checkArgIsOwning( + @OwningCollection Collection collection) {} void checkArgIsOwning(Socket @OwningCollection [] collection) {} - void checkArgIsOCwoO(@OwningCollectionWithoutObligation Collection collection) {} + void checkArgIsOCwoO( + @OwningCollectionWithoutObligation + Collection collection) {} void checkArgIsOCwoO(Socket @OwningCollectionWithoutObligation [] collection) {} } From c20c0d7f4978095c5ec6b0f5cc72def722923c3a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 2 Jun 2025 14:16:41 +0200 Subject: [PATCH 103/374] allow manual annotations on wildcard/typevar upper bounds to override default --- .../MustCallAnnotatedTypeFactory.java | 81 ++++++++++++++++--- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index e3bb46092498..2b1a0fbd5f3a 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -25,6 +25,8 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; import org.checkerframework.checker.mustcall.qual.MustCall; @@ -183,14 +185,32 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar continue; } } - AnnotationMirror mcAnno = typeArg.getEffectiveAnnotation(); + AnnotationMirror mcAnno = typeArg.getEffectiveAnnotationInHierarchy(TOP); boolean typeArgIsMcUnknown = mcAnno != null && processingEnv .getTypeUtils() .isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType()); if (typeArgIsMcUnknown) { - typeArg.replaceAnnotation(BOTTOM); + // check whether the upper bounds have manual MustCallUnknown annotations, in which case + // they should + // not be reset to bottom. For example like this: + // void m(List) {} + + if (typeArg.getUnderlyingType() instanceof WildcardType) { + TypeMirror extendsBound = + ((WildcardType) typeArg.getUnderlyingType()).getExtendsBound(); + if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) { + typeArg.replaceAnnotation(BOTTOM); + } + } else if (typeArg.getUnderlyingType() instanceof TypeVariable) { + TypeMirror upperBound = ((TypeVariable) typeArg.getUnderlyingType()).getUpperBound(); + if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(upperBound)) { + typeArg.replaceAnnotation(BOTTOM); + } + } else { + typeArg.replaceAnnotation(BOTTOM); + } } } else if (typeArg.getKind() == TypeKind.DECLARED) { replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg); @@ -200,13 +220,23 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar } /* - * Changes the type parameters of collections and iteratos to @MustCall if they are currently + * Changes the type parameter annotations of collections and iterators to @MustCall if they are currently * @MustCallUnknown. * *

This is necessary, as the type variable upper bounds for collections is * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and * imprecise. + * + *

This method changes the type parameter annotations for declared types directly. The other overload + * with access to {@code Element}s handles type parameter annotations for method return types and parameters, + * such that the changes are 'visible' at call-site as well as within the method. Changing this on the + * {@code Tree} is not sufficient. + * The reason that declared types are handled here is that for object initializations where the type + * parameter is left for inference, we don't want to change the type parameter annotation here, but wait + * for the inference instead, which instantiates it with the inferred type and corresponding annotation. + * {@code new Object<>()} + * Access to the {@code Tree} allows us to detect whether we have a new class tree without type parameters. */ @Override public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { @@ -215,7 +245,38 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool if (tree.getKind() != Tree.Kind.CLASS && tree.getKind() != Tree.Kind.INTERFACE) { if (type.getKind() == TypeKind.DECLARED) { replaceCollectionTypeVarsWithBottomIfTop(tree, (AnnotatedDeclaredType) type); - } else if (type.getKind() == TypeKind.EXECUTABLE) { + } + } + } + + /* + * Changes the type parameter annotations of collections and iterators to @MustCall if they are currently + * @MustCallUnknown. + * + *

This is necessary, as the type variable upper bounds for collections is + * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, + * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and + * imprecise. + * + *

This method changes the type parameter annotations of {@code Element}s, which is the preferred + * way for method return types and parameters, such that the changes are 'visible' at call-site as well + * as within the method. The type parameter annotations at other places is handled in the overload of this + * method with access to the {@code Tree}. The reason is that for object initializations where the type + * parameter is left for inference, we don't want to change the type parameter annotation here, but wait + * for the inference instead, which instantiates it with the inferred type and corresponding annotation. + * {@code new Object<>()} + */ + @Override + public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { + super.addComputedTypeAnnotations(elt, type); + + if (type.getKind() == TypeKind.EXECUTABLE) { + String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); + if (enclosingClass.startsWith("java.")) { + // this is a jdk method - do not change the upper bound. Defaults are only for user code. + // JDK methods have already been manually annotated. + return; + } else { AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; AnnotatedTypeMirror returnType = methodType.getReturnType(); @@ -223,16 +284,10 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) returnType); } - Element elt = TreeUtils.elementFromTree(tree); if (elt != null && elt instanceof ExecutableElement) { - String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); - if (enclosingClass.startsWith("java.")) { - // this is a jdk method - do not change the upper bound - } else { - for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { - if (paramType.getKind() == TypeKind.DECLARED) { - replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) paramType); - } + for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { + if (paramType.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) paramType); } } } From e67072be0bb199a366c96906d6f2778731259f10 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 4 Jun 2025 11:44:12 +0200 Subject: [PATCH 104/374] add NotOwning anno for Iterator#add override --- .../framework/test/diagnostics/JavaDiagnosticReader.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index cbf79c8d6f21..ac5c55b1213e 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -14,6 +14,7 @@ import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -233,7 +234,7 @@ public void remove() { } @Override - public TestDiagnosticLine next() { + public @NotOwning TestDiagnosticLine next() { if (nextLine == null) { throw new NoSuchElementException(); } From 6c59665d194452ffccc08886a71dca26763982bc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 14 Jun 2025 23:48:33 +0200 Subject: [PATCH 105/374] definitely set captured wildcard type vars to bottom --- .../MustCallAnnotatedTypeFactory.java | 38 ++++++++----------- .../resourceleak/ResourceLeakUtils.java | 20 ++++++++++ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 2b1a0fbd5f3a..1d27dd377c14 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -25,7 +25,6 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; @@ -48,6 +47,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.QualifierUpperBounds; @@ -203,9 +203,12 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) { typeArg.replaceAnnotation(BOTTOM); } - } else if (typeArg.getUnderlyingType() instanceof TypeVariable) { - TypeMirror upperBound = ((TypeVariable) typeArg.getUnderlyingType()).getUpperBound(); - if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(upperBound)) { + } else if (typeArg instanceof AnnotatedTypeVariable) { + AnnotatedTypeMirror upperBound = ((AnnotatedTypeVariable) typeArg).getUpperBound(); + // set back to bottom if the type var is a captured wildcard + // or if it doesn't have a manual MustCallUnknown anno + if (typeArg.containsCapturedTypes() + || !ResourceLeakUtils.hasManualMustCallUnknownAnno(upperBound, this)) { typeArg.replaceAnnotation(BOTTOM); } } else { @@ -271,25 +274,16 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { super.addComputedTypeAnnotations(elt, type); if (type.getKind() == TypeKind.EXECUTABLE) { - String enclosingClass = ElementUtils.getEnclosingClassName((ExecutableElement) elt); - if (enclosingClass.startsWith("java.")) { - // this is a jdk method - do not change the upper bound. Defaults are only for user code. - // JDK methods have already been manually annotated. - return; - } else { - AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; - AnnotatedTypeMirror returnType = methodType.getReturnType(); - - if (returnType.getKind() == TypeKind.DECLARED) { - replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) returnType); - } + AnnotatedExecutableType methodType = (AnnotatedExecutableType) type; + AnnotatedTypeMirror returnType = methodType.getReturnType(); - if (elt != null && elt instanceof ExecutableElement) { - for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { - if (paramType.getKind() == TypeKind.DECLARED) { - replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) paramType); - } - } + if (returnType.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) returnType); + } + + for (AnnotatedTypeMirror paramType : methodType.getParameterTypes()) { + if (paramType.getKind() == TypeKind.DECLARED) { + replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) paramType); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 91999bb60fb8..8f2ffeca7421 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -349,4 +349,24 @@ public static boolean hasManualMustCallUnknownAnno(TypeMirror typeMirror) { } return false; } + + /** + * Return true if the passed {@code AnnotatedTypeMirror} has a manual {@code MustCallUnknown} + * annotation. + * + * @param annotatedTypeMirror the {@code AnnotatedTypeMirror} + * @param mcAtf the annotated type factory of the MustCall type system + * @return true if the passed {@code AnnotatedTypeMirror} has a manual {@code MustCallUnknown} + * annotation + */ + public static boolean hasManualMustCallUnknownAnno( + AnnotatedTypeMirror annotatedTypeMirror, MustCallAnnotatedTypeFactory mcAtf) { + if (annotatedTypeMirror == null) return false; + AnnotationMirror manualMcAnno = annotatedTypeMirror.getPrimaryAnnotationInHierarchy(mcAtf.TOP); + if (manualMcAnno == null) return false; + if (AnnotationUtils.areSameByName(manualMcAnno, MustCallUnknown.class.getCanonicalName())) { + return true; + } + return false; + } } From 885e51599a5bedfc4561476214f9f5fc38da7d5c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 14 Jun 2025 23:49:11 +0200 Subject: [PATCH 106/374] suppress obscure receiver override warning --- .../org/checkerframework/javacutil/AnnotationMirrorSet.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java index 5ae0641b6838..a5a40d346ad4 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java @@ -122,6 +122,7 @@ public static AnnotationMirrorSet emptySet() { // Set methods @Override + // @SuppressWarnings("collectionownership:override.receiver") public int size() { return shadowSet.size(); } @@ -156,6 +157,7 @@ public Object[] toArray() { } @SuppressWarnings("keyfor:argument") // delegation + // @SuppressWarnings("collectionownership:override.receiver") @Override public boolean add( @UnknownInitialization(AnnotationMirrorSet.class) AnnotationMirrorSet this, From 3c5962b4ac4633eab49f224fc03f736a45e102af Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 15 Jun 2025 00:25:49 +0200 Subject: [PATCH 107/374] remove unneeded warning suppression --- .../org/checkerframework/common/value/ValueCheckerUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java index 1063b0a916d1..c9eb7bafa03a 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java @@ -173,7 +173,6 @@ private static T convertLongToType(long value, Class expectedType) { * @param origValues the objects to format * @return a list of the formatted objects */ - @SuppressWarnings("mustcall:type.arguments.not.inferred") private static @Nullable List convertToStringVal( List origValues) { if (origValues == null) { From 1ab4c28beb4bf7817d32186ddcba867c210df337 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 15 Jun 2025 00:43:47 +0200 Subject: [PATCH 108/374] suppress obscure receiver override warning --- .../org/checkerframework/javacutil/AnnotationMirrorSet.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java index a5a40d346ad4..29985259894d 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java @@ -122,7 +122,7 @@ public static AnnotationMirrorSet emptySet() { // Set methods @Override - // @SuppressWarnings("collectionownership:override.receiver") + @SuppressWarnings("collectionownership:override.receiver") public int size() { return shadowSet.size(); } @@ -156,8 +156,8 @@ public Object[] toArray() { return shadowSet.toArray(a); } - @SuppressWarnings("keyfor:argument") // delegation - // @SuppressWarnings("collectionownership:override.receiver") + @SuppressWarnings({"keyfor:argument", // delegation + "collectionownership:override.receiver"}) @Override public boolean add( @UnknownInitialization(AnnotationMirrorSet.class) AnnotationMirrorSet this, From ff9b5e579b0a84416ba7e685a25bd36d068033cd Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 15 Jun 2025 09:55:47 +0200 Subject: [PATCH 109/374] formatting --- .../org/checkerframework/javacutil/AnnotationMirrorSet.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java index 29985259894d..0cea89359ab0 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java @@ -156,8 +156,10 @@ public Object[] toArray() { return shadowSet.toArray(a); } - @SuppressWarnings({"keyfor:argument", // delegation - "collectionownership:override.receiver"}) + @SuppressWarnings({ + "keyfor:argument", // delegation + "collectionownership:override.receiver" + }) @Override public boolean add( @UnknownInitialization(AnnotationMirrorSet.class) AnnotationMirrorSet this, From 1efdb73849b919b9fef2888ccaec30ecee9f63fa Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:21:07 +0200 Subject: [PATCH 110/374] reformulate isResourceCollection in terms of TM vs ATM --- ...llectionOwnershipAnnotatedTypeFactory.java | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 442d6ed53bf2..1135c340d6a1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -143,16 +143,37 @@ public void postAnalyze(ControlFlowGraph cfg) { * @param t the AnnotatedTypeMirror * @return whether t is a resource collection */ - public boolean isResourceCollection(AnnotatedTypeMirror t) { - boolean isCollectionType = ResourceLeakUtils.isCollection(t.getUnderlyingType()); + public boolean isResourceCollection(TypeMirror t) { + List list = getMustCallValuesOfResourceCollectionComponent(t); + return list != null && list.size() > 0; + } + + /** + * If the given type is a collection, this method returns the MustCall values of its elements or + * null if there are none or if the given type is not a collection. + * + *

That is: + * + *

    + *
  1. if the given type is an array type, this method returns the MustCall values of its + * component type if there are any or else null. + *
  2. if the given type is a Java.util.Collection implementation, this method returns the + * MustCall values of its type variable upper bound if there are any or else null. + *
+ * + * @param t the AnnotatedTypeMirror + * @return if the given type is a collection, returns the MustCall values of its elements or null + * if there are none or if the given type is not a collection. + */ + public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) { + boolean isCollectionType = ResourceLeakUtils.isCollection(t); boolean isArrayType = t.getKind() == TypeKind.ARRAY; TypeMirror componentType = null; if (isArrayType) { - componentType = ((ArrayType) t.getUnderlyingType()).getComponentType(); + componentType = ((ArrayType) t).getComponentType(); } else if (isCollectionType) { - List typeArgs = - ((DeclaredType) t.getUnderlyingType()).getTypeArguments(); + List typeArgs = ((DeclaredType) t).getTypeArguments(); if (typeArgs.size() != 0) { componentType = typeArgs.get(0); } @@ -162,9 +183,9 @@ public boolean isResourceCollection(AnnotatedTypeMirror t) { if (componentType != null) { List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); - return list != null && list.size() > 0; + return list; } else { - return false; + return null; } } @@ -201,7 +222,7 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { AnnotatedTypeMirror returnType = t.getReturnType(); - if (isResourceCollection(returnType)) { + if (isResourceCollection(returnType.getUnderlyingType())) { AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { returnType.replaceAnnotation( @@ -210,7 +231,7 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void } for (AnnotatedTypeMirror paramType : t.getParameterTypes()) { - if (isResourceCollection(paramType)) { + if (isResourceCollection(paramType.getUnderlyingType())) { AnnotationMirror manualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { paramType.replaceAnnotation( @@ -237,7 +258,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { if (elt instanceof VariableElement) { boolean isField = elt.getKind() == ElementKind.FIELD; boolean isParam = elt.getKind() == ElementKind.PARAMETER; - boolean isResourceCollection = isResourceCollection(type); + boolean isResourceCollection = isResourceCollection(type.getUnderlyingType()); if (isResourceCollection) { if (isField) { @@ -275,7 +296,7 @@ public CollectionOwnershipTreeAnnotator( @Override public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { - if (isResourceCollection(type)) { + if (isResourceCollection(type.getUnderlyingType())) { type.replaceAnnotation(OWNINGCOLLECTION); } return super.visitNewArray(tree, type); From ede17f523f86ff7ac5e658bb0bc7d691942a66cb Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:21:26 +0200 Subject: [PATCH 111/374] add CollectionOwnershipType enum for pattern matching --- .../CollectionOwnershipAnnotatedTypeFactory.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 1135c340d6a1..70a1ca00314e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -62,6 +62,14 @@ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFa */ public final AnnotationMirror BOTTOM; + public enum CollectionOwnershipType { + NotOwningCollection, + OwningCollection, + OwningCollectionWithoutObligation, + OwningCollectionBottom, + None + }; + /** * Creates a CollectionOwnershipAnnotatedTypeFactory. * From 8e1842161fe73da4975c7624511dbebf06695910 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:23:30 +0200 Subject: [PATCH 112/374] add CollectionObligation subclass for collection obligation tracking --- .../MustCallConsistencyAnalyzer.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index bdb5567a38ec..d535c9c9d528 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -4,6 +4,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; @@ -37,6 +38,8 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; +import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -162,6 +165,9 @@ public class MustCallConsistencyAnalyzer { */ private final RLCCalledMethodsAnnotatedTypeFactory cmAtf; + /** The type factory for the Collection Ownership Checker. */ + private final CollectionOwnershipAnnotatedTypeFactory coAtf; + /** * A cache for the result of calling {@code RLCCalledMethodsAnnotatedTypeFactory.getStoreAfter()} * on a node. The cache prevents repeatedly computing least upper bounds on stores @@ -256,6 +262,48 @@ public Obligation(Set resourceAliases, Set whenTo this.whenToEnforce = ImmutableSet.copyOf(whenToEnforce); } + /** + * Returns a new Obligation. + * + *

We need this method since we frequently need to replace obligations. If the old obligation + * was of a certain subclass, we want the replacement to be as well. Dynamic dispatch then + * allows us to simply call getReplacement() on an obligation and get the replacement of the + * right (sub)class. + * + * @param resourceAliases set of resource aliases for the new obligation + * @return a new Obligation with the passed traits + */ + public Obligation getReplacement( + Set resourceAliases, Set whenToEnforce) { + return new Obligation(resourceAliases, whenToEnforce); + } + + /** + * Creates and returns an obligation derived from the given tree that is either an {@code + * ExpressionTree} or a {@code VariableTree}. + * + * @param tree the tree from which the Obligation is to be created. Must be ExpressionTree or + * VariableTree. + * @return an obligation derived from the given tree + */ + public static Obligation fromTree(Tree tree) { + JavaExpression jx = null; + Element elem = null; + if (tree instanceof ExpressionTree) { + jx = JavaExpression.fromTree((ExpressionTree) tree); + elem = TreeUtils.elementFromTree((ExpressionTree) tree); + } else if (tree instanceof VariableTree) { + jx = JavaExpression.fromVariableTree((VariableTree) tree); + elem = TreeUtils.elementFromDeclaration((VariableTree) tree); + } else { + throw new IllegalArgumentException( + "Tree must be ExpressionTree or VariableTree but is " + tree.getClass()); + } + return new Obligation( + ImmutableSet.of(new ResourceAlias(jx, elem, tree)), + Collections.singleton(MethodExitKind.NORMAL_RETURN)); + } + /** * Returns the resource alias in this Obligation's resource alias set corresponding to {@code * localVariableNode} if one is present. Otherwise, returns null. @@ -303,6 +351,26 @@ private boolean canBeSatisfiedThrough(LocalVariableNode localVariableNode) { return getResourceAlias(localVariableNode) != null; } + // /** + // * Returns true if this contains a resource alias corresponding to {@code localVariableNode}, + // * meaning that calling the required methods on {@code localVariableNode} is sufficient to + // * satisfy the must-call obligation this object represents. + // * + // * @param tree a local variable tree + // * @return true if a resource alias corresponding to {@code tree} is present + // */ + // private boolean canBeSatisfiedThrough(Tree tree) { + // for (ResourceAlias alias : resourceAliases) { + // if (alias.tree.equals(tree) + // || ((tree instanceof ExpressionTree) + // && JavaExpression.fromTree((ExpressionTree) tree) != null + // && alias.reference.equals(JavaExpression.fromTree((ExpressionTree) tree)))) { + // return true; + // } + // } + // return false; + // } + /** * Does this Obligation contain any resource aliases that were derived from {@link * MustCallAlias} parameters? @@ -425,6 +493,64 @@ public int hashCode() { } } + /** Obligation for a collection. To be fulfilled on its elements. */ + static class CollectionObligation extends Obligation { + + private String mustCallMethod; + + /** + * Create a CollectionObligation from a set of resource aliases. + * + * @param resourceAliases a set of resource aliases + * @param whenToEnforce when this Obligation should be enforced + */ + public CollectionObligation( + String mustCallMethod, + Set resourceAliases, + Set whenToEnforce) { + super(resourceAliases, whenToEnforce); + this.mustCallMethod = mustCallMethod; + } + + /** + * Create a CollectionObligation from an Obligation + * + * @param obligation the obligation to create a CollectionObligation from + */ + private CollectionObligation(Obligation obligation) { + super(obligation.resourceAliases, obligation.whenToEnforce); + } + + /** + * Creates and returns a CollectionObligation derived from the given tree that is either an + * {@code ExpressionTree} or a {@code VariableTree}. + * + * @param tree the tree from which the CollectionObligation is to be created. Must be + * ExpressionTree or VariableTree. + * @return a CollectionObligation derived from the given tree + */ + public static CollectionObligation fromTree(Tree tree) { + return new CollectionObligation(Obligation.fromTree(tree)); + } + + /** + * Returns a new CollectionObligation. + * + *

We need this method since we frequently need to replace obligations. If the old obligation + * was a CollectionObligation, we want the replacement to be as well. Dynamic dispatch then + * allows us to simply call getReplacement() on an obligation and get the replacement of the + * right class. + * + * @param resourceAliases set of resource aliases for the new obligation + * @return a new CollectionObligation with the passed traits + */ + @Override + public CollectionObligation getReplacement( + Set resourceAliases, Set whenToEnforce) { + return new CollectionObligation(this.mustCallMethod, resourceAliases, whenToEnforce); + } + } + // Is there a different Obligation on every line of the program, or is Obligation mutable? // (Or maybe Obligation is abstractly mutable when you consider the @MustCall types that are not // recorded in Obligation's representation.) Could you clarify? I found the first paragraph @@ -558,6 +684,7 @@ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc) { this.cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) ResourceLeakUtils.getRLCCalledMethodsChecker(rlc).getTypeFactory(); + this.coAtf = ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(cmAtf); this.checker = rlc; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); From b6b5162415e7169cf5ac587f9e588a70e8eafe7f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:23:46 +0200 Subject: [PATCH 113/374] add obligations for @OwningCollection parameters --- .../MustCallConsistencyAnalyzer.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index d535c9c9d528..afb59bf6439f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2343,6 +2343,11 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { if (cfg.getUnderlyingAST().getKind() == Kind.METHOD) { MethodTree method = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); Set result = new LinkedHashSet<>(1); + CFStore coStore = coAtf.getInput(cfg.getEntryBlock()).getRegularStore(); + if (coStore == null) { + throw new BugInCF("Cannot get initial store for " + cfg); + } + for (VariableTree param : method.getParameters()) { VariableElement paramElement = TreeUtils.elementFromDeclaration(param); boolean hasMustCallAlias = cmAtf.hasMustCallAlias(paramElement); @@ -2360,12 +2365,53 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { // method. incrementNumMustCall(paramElement); } + CFValue paramCfVal = coStore.getValue(JavaExpression.fromVariableTree(param)); + CollectionOwnershipType cotype = getCoType(paramCfVal); + if (cotype == CollectionOwnershipType.OwningCollection) { + List mustCallValues = + coAtf.getMustCallValuesOfResourceCollectionComponent(paramCfVal.getUnderlyingType()); + if (mustCallValues == null) { + throw new BugInCF( + "List of MustCall values of component type is null for OwningCollection parameter: " + + param); + } + for (String mustCallMethod : mustCallValues) { + result.add( + new CollectionObligation( + mustCallMethod, + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), + paramElement, + param, + hasMustCallAlias)), + Collections.singleton(MethodExitKind.NORMAL_RETURN))); + } + } } return result; } return Collections.emptySet(); } + private CollectionOwnershipType getCoType(CFValue val) { + if (val == null) { + return CollectionOwnershipType.None; + } + for (AnnotationMirror anm : val.getAnnotations()) { + if (AnnotationUtils.areSame(anm, coAtf.NOTOWNINGCOLLECTION)) { + return CollectionOwnershipType.NotOwningCollection; + } else if (AnnotationUtils.areSame(anm, coAtf.OWNINGCOLLECTION)) { + return CollectionOwnershipType.OwningCollection; + } else if (AnnotationUtils.areSame(anm, coAtf.OWNINGCOLLECTIONWITHOUTOBLIGATION)) { + return CollectionOwnershipType.OwningCollectionWithoutObligation; + } else if (AnnotationUtils.areSame(anm, coAtf.BOTTOM)) { + return CollectionOwnershipType.OwningCollectionBottom; + } + } + return CollectionOwnershipType.None; + } + /** * Checks whether there is some resource alias set R in {@code obligations} such that * R contains a {@link ResourceAlias} whose local variable is {@code node}. From 9921bfa0f8f7698f0e1fc31b1ebb076dd8fe2aa2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:32:44 +0200 Subject: [PATCH 114/374] add method to get store for entire block in GenericAtf --- .../type/GenericAnnotatedTypeFactory.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 d9ef88e10e4c..a319216bba39 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -61,6 +61,7 @@ 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.block.Block; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; @@ -1213,6 +1214,23 @@ public Store getStoreBefore(Set nodes) { return merge; } + /** + * Returns the regular store for a given block. + * + * @param block a block whose regular store to return + * @return the regular store of {@code block}. + */ + public Store getRegularStore(Block block) { + TransferInput input; + if (!analysis.isRunning()) { + input = flowResult.getInput(block); + } else { + input = analysis.getInput(block); + } + + return input.getRegularStore(); + } + /** * Returns the store immediately before a given node. * From c40ce9aa6f5382c0b9e221831ca21940311ba809 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:32:55 +0200 Subject: [PATCH 115/374] fix call to right method to get store --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index afb59bf6439f..f9a528bb83a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2343,7 +2343,7 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { if (cfg.getUnderlyingAST().getKind() == Kind.METHOD) { MethodTree method = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); Set result = new LinkedHashSet<>(1); - CFStore coStore = coAtf.getInput(cfg.getEntryBlock()).getRegularStore(); + CFStore coStore = coAtf.getRegularStore(cfg.getEntryBlock()); if (coStore == null) { throw new BugInCF("Cannot get initial store for " + cfg); } From af48e9edcba1ab21920c527cb41c2bd933a73dbf Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 16 Jun 2025 15:33:22 +0200 Subject: [PATCH 116/374] add basic typing tests for co type system --- .../CollectionOwnershipBasicTyping.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java new file mode 100644 index 000000000000..45d58e75851d --- /dev/null +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -0,0 +1,36 @@ +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +/* + * Test whether the defaults of resource collection fields, parameters, return types and new +allocations + * are as expected. + */ +class CollectionOwnershipBasicTyping { + + int n = 10; + + /* + * Check that this return is allowed. The local list defaults to @OwningCollection, which + * the return type does too. Thus, return value is consistent with the return type. + */ + Collection checkReturn() { + List list = new ArrayList<>(); + return list; + } + + /* + * Check that this return is disallowed. The parameter list defaults to @NotOwningCollection, + * which is a supertype of the @OwningCollection return type. + * Thus, return value is inconsistent with the return type. + */ + Collection checkReturn(List list) { + // :: error: return + return list; + } +} From 0ced652fc9ae99949c82d9fb33d5b619be26b584 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 08:23:03 +0200 Subject: [PATCH 117/374] add obligations for @OC return value --- ...llectionOwnershipAnnotatedTypeFactory.java | 24 +++++ .../MustCallConsistencyAnalyzer.java | 92 +++++++++++++------ .../resourceleak/ResourceLeakUtils.java | 28 ++++++ .../checker/resourceleak/messages.properties | 1 + 4 files changed, 119 insertions(+), 26 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 70a1ca00314e..3a66f617e0c9 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -4,6 +4,7 @@ import com.sun.source.tree.NewArrayTree; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -32,6 +33,7 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; @@ -197,6 +199,28 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) } } + public CollectionOwnershipType getCoType(CFValue val) { + return val == null ? CollectionOwnershipType.None : getCoType(val.getAnnotations()); + } + + public CollectionOwnershipType getCoType(Collection annos) { + if (annos == null) { + return CollectionOwnershipType.None; + } + for (AnnotationMirror anm : annos) { + if (AnnotationUtils.areSame(anm, NOTOWNINGCOLLECTION)) { + return CollectionOwnershipType.NotOwningCollection; + } else if (AnnotationUtils.areSame(anm, OWNINGCOLLECTION)) { + return CollectionOwnershipType.OwningCollection; + } else if (AnnotationUtils.areSame(anm, OWNINGCOLLECTIONWITHOUTOBLIGATION)) { + return CollectionOwnershipType.OwningCollectionWithoutObligation; + } else if (AnnotationUtils.areSame(anm, BOTTOM)) { + return CollectionOwnershipType.OwningCollectionBottom; + } + } + return CollectionOwnershipType.None; + } + @Override protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator( diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index f9a528bb83a2..3e478959c26a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -496,7 +496,7 @@ public int hashCode() { /** Obligation for a collection. To be fulfilled on its elements. */ static class CollectionObligation extends Obligation { - private String mustCallMethod; + public String mustCallMethod; /** * Create a CollectionObligation from a set of resource aliases. @@ -729,6 +729,38 @@ public void analyze(ControlFlowGraph cfg) { } } + /** + * Adds {@code CollectionObligation}s if the return type is {@code @OwningCollection}. + * + * @param obligations the set of tracked obligations + * @node the node the check + */ + private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { + LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); + if (tmpVar != null) { + CFStore coStore = coAtf.getStoreAfter(node); + CFValue returnCfVal = coStore.getValue(JavaExpression.fromNode(tmpVar)); + CollectionOwnershipType cotype = coAtf.getCoType(returnCfVal); + if (cotype == CollectionOwnershipType.OwningCollection) { + ResourceAlias tmpVarAsResourceAlias = + new ResourceAlias(new LocalVariable(tmpVar), node.getTree()); + List mustCallValues = + coAtf.getMustCallValuesOfResourceCollectionComponent(returnCfVal.getUnderlyingType()); + if (mustCallValues == null) { + throw new BugInCF( + "List of MustCall values of component type is null for OwningCollection return value: " + + node); + } + for (String mustCallMethod : mustCallValues) { + System.out.println("adding obl for method: " + mustCallMethod); + obligations.add( + new CollectionObligation( + mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); + } + } + } + } + /** * Update a set of Obligations to account for a method or constructor invocation. * @@ -751,6 +783,8 @@ private void updateObligationsForInvocation( incrementNumMustCall(node); } + addObligationsForOwningCollectionReturn(obligations, node); + if (!shouldTrackInvocationResult(obligations, node, false)) { return; } @@ -877,7 +911,7 @@ private boolean isValidCreatesMustCallForExpression( "tried to remove multiple sets containing a reset expression at once"); } toRemove = obligation; - toAdd = new Obligation(ImmutableSet.of(alias), obligation.whenToEnforce); + toAdd = obligation.getReplacement(ImmutableSet.of(alias), obligation.whenToEnforce); } } @@ -1006,7 +1040,7 @@ private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosin .toSet(); obligations.remove(obligationContainingMustCallAlias); obligations.add( - new Obligation( + obligationContainingMustCallAlias.getReplacement( newResourceAliasSet, obligationContainingMustCallAlias.whenToEnforce)); // It is not an error if there is no Obligation containing the must-call // alias. In that case, what has usually happened is that no Obligation was @@ -1166,6 +1200,13 @@ private void removeObligationsAtOwnershipTransferToParameters( obligations.remove(localObligation); } } + boolean paramHasManualOcAnno = + coAtf.getCoType(new HashSet<>(parameter.asType().getAnnotationMirrors())) + == CollectionOwnershipType.OwningCollection; + if (paramHasManualOcAnno) { + Obligation localObligation = getObligationForVar(obligations, local); + obligations.remove(localObligation); + } } } } @@ -1373,7 +1414,7 @@ private void addAliasToObligationsContainingVar( Set newAliases = new LinkedHashSet<>(obligation.resourceAliases); newAliases.add(newAlias); - newObligations.add(new Obligation(newAliases, obligation.whenToEnforce)); + newObligations.add(obligation.getReplacement(newAliases, obligation.whenToEnforce)); } } @@ -1444,7 +1485,7 @@ private void removeObligationsContainingVar( whenToEnforce.removeAll(whatToClear); if (!whenToEnforce.isEmpty()) { - newObligations.add(new Obligation(obligation.resourceAliases, whenToEnforce)); + newObligations.add(obligation.getReplacement(obligation.resourceAliases, whenToEnforce)); } } } @@ -1539,7 +1580,8 @@ private void removeObligationsContainingVar( replacements.put(obligation, null); } else { replacements.put( - obligation, new Obligation(newResourceAliasesForObligation, obligation.whenToEnforce)); + obligation, + obligation.getReplacement(newResourceAliasesForObligation, obligation.whenToEnforce)); } } @@ -2269,7 +2311,8 @@ private void propagateObligationsToSuccessorBlock( Set copyOfResourceAliases = new LinkedHashSet<>(obligation.resourceAliases); copyOfResourceAliases.removeIf( alias -> !aliasInScopeInSuccessor(regularStoreOfSuccessor, alias)); - successorObligations.add(new Obligation(copyOfResourceAliases, obligation.whenToEnforce)); + successorObligations.add( + obligation.getReplacement(copyOfResourceAliases, obligation.whenToEnforce)); } } @@ -2366,7 +2409,7 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { incrementNumMustCall(paramElement); } CFValue paramCfVal = coStore.getValue(JavaExpression.fromVariableTree(param)); - CollectionOwnershipType cotype = getCoType(paramCfVal); + CollectionOwnershipType cotype = coAtf.getCoType(paramCfVal); if (cotype == CollectionOwnershipType.OwningCollection) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(paramCfVal.getUnderlyingType()); @@ -2394,24 +2437,6 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { return Collections.emptySet(); } - private CollectionOwnershipType getCoType(CFValue val) { - if (val == null) { - return CollectionOwnershipType.None; - } - for (AnnotationMirror anm : val.getAnnotations()) { - if (AnnotationUtils.areSame(anm, coAtf.NOTOWNINGCOLLECTION)) { - return CollectionOwnershipType.NotOwningCollection; - } else if (AnnotationUtils.areSame(anm, coAtf.OWNINGCOLLECTION)) { - return CollectionOwnershipType.OwningCollection; - } else if (AnnotationUtils.areSame(anm, coAtf.OWNINGCOLLECTIONWITHOUTOBLIGATION)) { - return CollectionOwnershipType.OwningCollectionWithoutObligation; - } else if (AnnotationUtils.areSame(anm, coAtf.BOTTOM)) { - return CollectionOwnershipType.OwningCollectionBottom; - } - } - return CollectionOwnershipType.None; - } - /** * Checks whether there is some resource alias set R in {@code obligations} such that * R contains a {@link ResourceAlias} whose local variable is {@code node}. @@ -2463,6 +2488,21 @@ private static boolean varTrackedInObligations( private void checkMustCall( Obligation obligation, AccumulationStore cmStore, CFStore mcStore, String outOfScopeReason) { + if (obligation instanceof CollectionObligation) { + ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); + if (!reportedErrorAliases.contains(firstAlias)) { + if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { + reportedErrorAliases.add(firstAlias); + checker.reportError( + firstAlias.tree, + "unfulfilled.collection.obligations", + ((CollectionObligation) obligation).mustCallMethod, + firstAlias.stringForErrorMessage(), + outOfScopeReason); + } + } + } + Map> mustCallValues = obligation.getMustCallMethods(cmAtf, mcStore); // Optimization: if mustCallValues is null, always issue a warning (there is no way to diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 8f2ffeca7421..41e5998feca6 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -21,6 +21,7 @@ import org.checkerframework.checker.mustcall.qual.MustCall; import org.checkerframework.checker.mustcall.qual.MustCallUnknown; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; @@ -179,6 +180,33 @@ public static RLCCalledMethodsChecker getRLCCalledMethodsChecker(SourceChecker r } } + /** + * Given a type factory that is part of the resource leak checker hierarchy, returns the {@link + * RLCCalledMethodsAnnotatedTypeFactory} in the checker hierarchy. + * + * @param referenceAtf the type factory to retrieve the {@link + * RLCCalledMethodsAnnotatedTypeFactory} from; must be part of the Resource Leak hierarchy + * @return the {@link RLCCalledMethodsAnnotatedTypeFactory} in the checker hierarchy + */ + public static RLCCalledMethodsAnnotatedTypeFactory getRLCCalledMethodsAnnotatedTypeFactory( + AnnotatedTypeFactory referenceAtf) { + return getRLCCalledMethodsAnnotatedTypeFactory(referenceAtf.getChecker()); + } + + /** + * Given a checker that is part of the resource leak checker hierarchy, returns the {@link + * RLCCalledMethodsAnnotatedTypeFactory} in the hierarchy. + * + * @param referenceChecker the checker to retrieve the {@link + * RLCCalledMethodsAnnotatedTypeFactory} from; must be part of the Resource Leak hierarchy + * @return the {@link RLCCalledMethodsAnnotatedTypeFactory} in the hierarchy + */ + public static RLCCalledMethodsAnnotatedTypeFactory getRLCCalledMethodsAnnotatedTypeFactory( + SourceChecker referenceChecker) { + return (RLCCalledMethodsAnnotatedTypeFactory) + getRLCCalledMethodsChecker(referenceChecker).getTypeFactory(); + } + /** * Given a checker part of the resource leak ecosystem, returns the {@link * CollectionOwnershipAnnotatedTypeFactory} in the checker hierarchy. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index 8f2cbaa5d7ee..e788043f81f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -8,3 +8,4 @@ destructor.exceptional.postcondition=Method %s must resolve the must-call obliga mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope without being assigned into an owning field of this object (if this is a constructor) or returned.%nReason for going out of scope: %s mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in pairs (one on a return type and one on a parameter type).%nBut %s required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s +unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s From 6383e1e64d787aedb3ac5acb36059a7f926a393e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 08:24:49 +0200 Subject: [PATCH 118/374] remove ownership from rhs in assignment --- .../CollectionOwnershipTransfer.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index f876609351c4..8d5daa601343 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,15 +1,19 @@ package org.checkerframework.checker.collectionownership; import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; @@ -40,6 +44,50 @@ public CollectionOwnershipTransfer(CFAnalysis analysis) { mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(atypeFactory); } + @Override + public TransferResult visitAssignment( + AssignmentNode node, TransferInput in) { + TransferResult res = super.visitAssignment(node, in); + + CFStore store = res.getRegularStore(); + // Node lhs = node.getTarget(); + // lhs = getNodeOrTempVar(lhs); + Node rhs = node.getExpression(); + rhs = getNodeOrTempVar(rhs); + // JavaExpression lhsJx = JavaExpression.fromNode(lhs); + JavaExpression rhsJx = JavaExpression.fromNode(rhs); + + // CollectionOwnershipType lhsType = atypeFactory.getCoType(store.getValue(lhsJx)); + CollectionOwnershipType rhsType = atypeFactory.getCoType(store.getValue(rhsJx)); + + // ownership transfer from rhs into lhs + if (rhsType == CollectionOwnershipType.OwningCollection + || rhsType == CollectionOwnershipType.OwningCollectionWithoutObligation) { + store.clearValue(rhsJx); + store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } + + // boolean assignmentOfOwningCollectionArrayElement = + // lhsIsOwningCollection && lhs.getTree().getKind() == Tree.Kind.ARRAY_ACCESS; + + // if (assignmentOfOwningCollectionArrayElement) { + // ExpressionTree arrayExpression = ((ArrayAccessTree) lhs.getTree()).getExpression(); + // JavaExpression arrayJx = JavaExpression.fromTree(arrayExpression); + + // boolean inAssigningLoop = + // MustCallOnElementsAnnotatedTypeFactory.doesAssignmentCreateArrayObligation( + // (AssignmentTree) node.getTree()); + + // // transformation of assigning loop is handled at the loop condition node, + // // not the assignment node. So, only transform if not in an assigning loop. + // if (!inAssigningLoop) { + // store = + // transformWriteToOwningCollection(arrayJx, arrayExpression, node.getExpression(), + // store); + // } + return new RegularTransferResult(res.getResultValue(), store); + } + @Override public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { @@ -141,4 +189,23 @@ public void updateStoreWithTempVar(TransferResult result, Node } } } + + /** + * Removes casts from {@code node} and returns the temp-var corresponding to it if it exists or + * else {@code node} with removed casts. + * + * @param node the node + * @return the temp-var corresponding to {@code node} with casts removed if it exists or else + * {@code node} with casts removed + */ + protected Node getNodeOrTempVar(Node node) { + node = NodeUtils.removeCasts(node); + Node tempVarForNode = + ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(atypeFactory) + .getTempVarForNode(node); + if (tempVarForNode != null) { + return tempVarForNode; + } + return node; + } } From 6dc03c09db6d4a81b469271f55b42893291a992a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 08:25:13 +0200 Subject: [PATCH 119/374] adapt expected test outcomes --- checker/tests/resourceleak/Issue4815.java | 6 ++++++ checker/tests/resourceleak/SocketIntoList.java | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java index ec9178ad1e5e..602771335b97 100644 --- a/checker/tests/resourceleak/Issue4815.java +++ b/checker/tests/resourceleak/Issue4815.java @@ -8,6 +8,12 @@ public class Issue4815 { public void initialize( List list, @Owning @MustCall("initialize") T object) { object.initialize(); + // list resolves to List<@MustCall Component> and thus cannot accept + // an element of non-empty @MustCall type enforced by the Java + // typechecker. This is an unfortunate consequence of the otherwise + // elegant extension of the RLC to collections, which doesn't detect + // that object already fulfilled its obligation here. + // :: error: argument list.add(object); } diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index b68798f2fec4..6a34c8c1b45a 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -14,18 +14,24 @@ public void test1(List<@MustCall({}) Socket> l) { l.add(s); } - public void test2(@OwningCollection List l) { + public List test2(@OwningCollection List l) { // s is unconnected, so no error is expected when it's stored into the list. // But, if the list is unannotated, we do get an error at its declaration site // (as expected, due to #5912). Socket s = new Socket(); l.add(s); + return l; } public void test3(List<@MustCall({}) Socket> l) throws Exception { - // :: error: required.method.not.called + // although s is illegally assigned into the list l, an error + // for required.method.not.called is not additionally reported at + // this declaration site of s. list#add(@Owning E) takes on + // the obligation of the argument. Socket s = new Socket(); s.bind(new InetSocketAddress("192.168.0.1", 0)); + // l cannot hold elements with non-empty @MustCall values + // :: error: argument l.add(s); } From e07845289af10a3906045a63636c483540ed93d1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 09:47:51 +0200 Subject: [PATCH 120/374] implement ownership transfer for method invocations --- .../CollectionOwnershipTransfer.java | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 8d5daa601343..0423dbb7400c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,6 +1,10 @@ package org.checkerframework.checker.collectionownership; +import java.util.HashSet; +import java.util.List; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; @@ -18,6 +22,7 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** @@ -90,10 +95,15 @@ public TransferResult visitAssignment( @Override public TransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput in) { - TransferResult result = super.visitMethodInvocation(n, in); + MethodInvocationNode node, TransferInput in) { + TransferResult res = super.visitMethodInvocation(node, in); + + updateStoreWithTempVar(res, node); + + ExecutableElement method = node.getTarget().getMethod(); + List args = node.getArguments(); + res = transferOwnershipForMethodInvocation(method, args, res); - updateStoreWithTempVar(result, n); // if (!noCreatesMustCallFor) { // List targetExprs = // CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( @@ -116,7 +126,46 @@ public TransferResult visitMethodInvocation( // } // } // } - return result; + return res; + } + + /** + * Executes collection ownership transfer in method invocations. Owning arguments to an owning + * parameter lose ownership. + * + * @param method the method whose parameters are checked + * @param args the list of method arguments + * @param res the transfer result so far + * @return the updated transfer result + */ + private TransferResult transferOwnershipForMethodInvocation( + ExecutableElement method, List args, TransferResult res) { + List params = method.getParameters(); + + CFStore store = res.getRegularStore(); + for (int i = 0; i < Math.min(args.size(), params.size()); i++) { + VariableElement param = params.get(i); + Node arg = args.get(i); + arg = getNodeOrTempVar(arg); + JavaExpression argJx = JavaExpression.fromNode(arg); + CollectionOwnershipType argType = atypeFactory.getCoType(store.getValue(argJx)); + CollectionOwnershipType paramType = + atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); + + if (paramType == CollectionOwnershipType.OwningCollection) { + if (argType == CollectionOwnershipType.OwningCollectionWithoutObligation + || argType == CollectionOwnershipType.OwningCollection) { + store.clearValue(argJx); + store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + } + } else if (paramType == CollectionOwnershipType.OwningCollectionWithoutObligation) { + if (argType == CollectionOwnershipType.OwningCollectionWithoutObligation) { + store.clearValue(argJx); + store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + } + } + } + return new RegularTransferResult(res.getResultValue(), store); } // /** @@ -141,6 +190,10 @@ public TransferResult visitObjectCreation( ObjectCreationNode node, TransferInput input) { TransferResult result = super.visitObjectCreation(node, input); updateStoreWithTempVar(result, node); + + ExecutableElement constructor = TreeUtils.elementFromUse(node.getTree()); + List args = node.getArguments(); + result = transferOwnershipForMethodInvocation(constructor, args, result); return result; } From bbf48ceeb6ef5b47b6ab251bab974fd1f7c8fabe Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 19:01:33 +0200 Subject: [PATCH 121/374] add test for assignment transfering ownership --- .../CollectionOwnershipBasicTyping.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 45d58e75851d..23bb09d86df7 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -33,4 +33,25 @@ Collection checkReturn(List list) { // :: error: return return list; } + + void test() { + Collection col = new ArrayList<>(); + Collection col2 = new ArrayList<>(); + // col : @OwningCollection, col2 : @OwningCollection + col = col2; + // col : @OwningCollection, col2 : @NotOwningCollection + + // col2 is NotOwningCollection, so the second call should fail + checkArgIsOwning(col); + // :: error: argument + checkArgIsOwning(col2); + } + + void checkArgIsOwning( + @OwningCollection Collection collection) {} + + void checkArgIsOCwoO( + @OwningCollectionWithoutObligation + Collection collection) {} + } From d3b08d3b8b51fa5bc6abdbcf0cc575155bae650c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 21:52:34 +0200 Subject: [PATCH 122/374] ensure resource collections constructed with inferred type arg are set to @OwningCollection in COTransfer function --- .../CollectionOwnershipTransfer.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 0423dbb7400c..e2d3103b2400 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -5,6 +5,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; @@ -194,7 +195,27 @@ public TransferResult visitObjectCreation( ExecutableElement constructor = TreeUtils.elementFromUse(node.getTree()); List args = node.getArguments(); result = transferOwnershipForMethodInvocation(constructor, args, result); - return result; + + // the return value defaulting logic cannot recognize that a diamond constructed collection + // is a resource collection, as it runs before the type variable is inferred: + // List = new ArrayList<>(); + // Thus, the following checks object creation expressions again on whether they are + // resource collections with no type variables, and if they are Bottom, they are + // unrefined to @OwningCollection. + CFStore store = result.getRegularStore(); + Node tempVarNode = getNodeOrTempVar(node); + JavaExpression exprJx = JavaExpression.fromNode(tempVarNode); + CollectionOwnershipType resolvedType = atypeFactory.getCoType(store.getValue(exprJx)); + TypeMirror javaTypeOfExpr = TreeUtils.elementFromTree(tempVarNode.getTree()).asType(); + if (atypeFactory.isResourceCollection(javaTypeOfExpr)) { + boolean isDiamond = node.getTree().getTypeArguments().size() == 0; + if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { + store.clearValue(exprJx); + store.insertValue(exprJx, atypeFactory.OWNINGCOLLECTION); + } + } + + return new RegularTransferResult(result.getResultValue(), store); } // TODO sck: I think that I can use the temp var management from MC checker and don't From 6443c3fd9ef432842506550d486a53d33d5c2e01 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 17 Jun 2025 22:00:55 +0200 Subject: [PATCH 123/374] update co typing tests --- .../CollectionOwnershipBasicTyping.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 23bb09d86df7..767daa87bbe6 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -34,7 +34,7 @@ Collection checkReturn(List list) { return list; } - void test() { + void testAssignmentTransfersOwnership() { Collection col = new ArrayList<>(); Collection col2 = new ArrayList<>(); // col : @OwningCollection, col2 : @OwningCollection @@ -45,6 +45,27 @@ void test() { checkArgIsOwning(col); // :: error: argument checkArgIsOwning(col2); + + closeElements(col); + } + + /* + * Check that a resource collection constructed without explicit type argument also is of type @OwningCollection. + */ + void testDiamond() { + Collection col = new ArrayList<>(); + // :: error: arg + checkArgIsOCwoO(col); + closeElements(col); + } + + void closeElements(@OwningCollection Collection socketCollection) { + for (Socket s : socketCollection) { + try { + s.close(); + } catch (Exception e) { + } + } } void checkArgIsOwning( @@ -53,5 +74,4 @@ void checkArgIsOwning( void checkArgIsOCwoO( @OwningCollectionWithoutObligation Collection collection) {} - } From 53607864c98e97d7b70f8f696fddf7ee8e967315 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 18 Jun 2025 22:08:16 +0200 Subject: [PATCH 124/374] add dedicated Unknown method obligation for MustCallUnknown --- .../CollectionOwnershipAnnotatedTypeFactory.java | 6 ++++++ .../resourceleak/MustCallConsistencyAnalyzer.java | 5 ++++- .../checker/resourceleak/ResourceLeakUtils.java | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 3a66f617e0c9..84e016350d9f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -72,6 +72,12 @@ public enum CollectionOwnershipType { None }; + /** + * The method name used for CollectionObligations that represent an obligation of MustCallUnkown. + * The digit in the first character ensures this cannot coincide with an actual method name. + */ + public static final String UNKNOWN_METHOD_NAME = "1UNKNOWN"; + /** * Creates a CollectionOwnershipAnnotatedTypeFactory. * diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 3e478959c26a..4cfd5b96b2c7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2493,10 +2493,13 @@ private void checkMustCall( if (!reportedErrorAliases.contains(firstAlias)) { if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { reportedErrorAliases.add(firstAlias); + String methodName = ((CollectionObligation) obligation).mustCallMethod; checker.reportError( firstAlias.tree, "unfulfilled.collection.obligations", - ((CollectionObligation) obligation).mustCallMethod, + methodName == CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME + ? "Unknown" + : methodName, firstAlias.stringForErrorMessage(), outOfScopeReason); } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 41e5998feca6..556e5d3a3a80 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -342,9 +343,14 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem AnnotationMirror manualAnno = AnnotationUtils.getAnnotationByClass(type.getAnnotationMirrors(), MustCall.class); + AnnotationMirror manualMcUnkownAnno = + AnnotationUtils.getAnnotationByClass(type.getAnnotationMirrors(), MustCallUnknown.class); if (manualAnno != null) { return getValuesInAnno(manualAnno, mcAtf.getMustCallValueElement()); } + if (manualMcUnkownAnno != null) { + return Collections.singletonList(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME); + } TypeElement typeElement = TypesUtils.getTypeElement(type); if (typeElement == null) { @@ -353,12 +359,17 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem AnnotationMirror imcAnnotation = mcAtf.getDeclAnnotation(typeElement, InheritableMustCall.class); AnnotationMirror mcAnnotation = mcAtf.getDeclAnnotation(typeElement, MustCall.class); + AnnotationMirror mcUnknownAnnotation = + mcAtf.getDeclAnnotation(typeElement, MustCallUnknown.class); if (mcAnnotation != null) { return getValuesInAnno(mcAnnotation, mcAtf.getMustCallValueElement()); } if (imcAnnotation != null) { return getValuesInAnno(imcAnnotation, mcAtf.getInheritableMustCallValueElement()); } + if (mcUnknownAnnotation != null) { + return Collections.singletonList(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME); + } return new ArrayList<>(); } From 4c91e509cc238c0c59c053d3448c7378bf6cae43 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 18 Jun 2025 22:08:56 +0200 Subject: [PATCH 125/374] remove all obligations for a collection, not just one --- .../MustCallConsistencyAnalyzer.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4cfd5b96b2c7..57189d1e718f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -752,7 +752,6 @@ private void addObligationsForOwningCollectionReturn(Set obligations + node); } for (String mustCallMethod : mustCallValues) { - System.out.println("adding obl for method: " + mustCallMethod); obligations.add( new CollectionObligation( mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); @@ -1204,8 +1203,10 @@ private void removeObligationsAtOwnershipTransferToParameters( coAtf.getCoType(new HashSet<>(parameter.asType().getAnnotationMirrors())) == CollectionOwnershipType.OwningCollection; if (paramHasManualOcAnno) { - Obligation localObligation = getObligationForVar(obligations, local); - obligations.remove(localObligation); + Set obligationsForVar = getObligationsForVar(obligations, local); + for (Obligation obligation : obligationsForVar) { + obligations.remove(obligation); + } } } } @@ -2474,6 +2475,26 @@ private static boolean varTrackedInObligations( return null; } + /** + * Gets the set of Obligations whose resource alias set contain the given local variable, or an + * empty set if none exist. in {@code obligations}. + * + * @param obligations a set of Obligations + * @param node variable of interest + * @return the set of Obligations in {@code obligations} whose resource alias set contains {@code + * node}, or an empty set if there is no such Obligation + */ + /*package-private*/ static Set getObligationsForVar( + Set obligations, LocalVariableNode node) { + Set obligationsForVar = new HashSet<>(); + for (Obligation obligation : obligations) { + if (obligation.canBeSatisfiedThrough(node)) { + obligationsForVar.add(obligation); + } + } + return obligationsForVar; + } + /** * For the given Obligation, checks that at least one of its variables has its {@code @MustCall} * obligation satisfied, based on {@code @CalledMethods} and {@code @MustCall} types in the given From 9aa5354bdd936abe2c4cc73fcaccd3c17fd68e9a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 18 Jun 2025 22:09:30 +0200 Subject: [PATCH 126/374] change type of resourcecollection diamond expressions correctly --- .../CollectionOwnershipTransfer.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index e2d3103b2400..8090b3510898 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -64,7 +64,13 @@ public TransferResult visitAssignment( JavaExpression rhsJx = JavaExpression.fromNode(rhs); // CollectionOwnershipType lhsType = atypeFactory.getCoType(store.getValue(lhsJx)); - CollectionOwnershipType rhsType = atypeFactory.getCoType(store.getValue(rhsJx)); + CFValue rhsValue = null; + try { + rhsValue = store.getValue(rhsJx); + } catch (Exception e) { + return res; + } + CollectionOwnershipType rhsType = atypeFactory.getCoType(rhsValue); // ownership transfer from rhs into lhs if (rhsType == CollectionOwnershipType.OwningCollection @@ -149,7 +155,13 @@ private TransferResult transferOwnershipForMethodInvocation( Node arg = args.get(i); arg = getNodeOrTempVar(arg); JavaExpression argJx = JavaExpression.fromNode(arg); - CollectionOwnershipType argType = atypeFactory.getCoType(store.getValue(argJx)); + CFValue argValue = null; + try { + argValue = store.getValue(argJx); + } catch (Exception e) { + continue; + } + CollectionOwnershipType argType = atypeFactory.getCoType(argValue); CollectionOwnershipType paramType = atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); @@ -201,21 +213,25 @@ public TransferResult visitObjectCreation( // List = new ArrayList<>(); // Thus, the following checks object creation expressions again on whether they are // resource collections with no type variables, and if they are Bottom, they are - // unrefined to @OwningCollection. + // unrefined to @OwningCollection. Change the type of both the type var and the computed + // expression itself. CFStore store = result.getRegularStore(); + CFValue resultValue = result.getResultValue(); Node tempVarNode = getNodeOrTempVar(node); - JavaExpression exprJx = JavaExpression.fromNode(tempVarNode); - CollectionOwnershipType resolvedType = atypeFactory.getCoType(store.getValue(exprJx)); + JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); + CollectionOwnershipType resolvedType = atypeFactory.getCoType(store.getValue(tempVarJx)); TypeMirror javaTypeOfExpr = TreeUtils.elementFromTree(tempVarNode.getTree()).asType(); if (atypeFactory.isResourceCollection(javaTypeOfExpr)) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { - store.clearValue(exprJx); - store.insertValue(exprJx, atypeFactory.OWNINGCOLLECTION); + store.clearValue(tempVarJx); + store.insertValue(tempVarJx, atypeFactory.OWNINGCOLLECTION); + resultValue = + analysis.createSingleAnnotationValue(atypeFactory.OWNINGCOLLECTION, node.getType()); } } - return new RegularTransferResult(result.getResultValue(), store); + return new RegularTransferResult(resultValue, store); } // TODO sck: I think that I can use the temp var management from MC checker and don't From e5e5cddd55a2d6e9c45d406b5c722452e02236ed Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 18 Jun 2025 22:09:39 +0200 Subject: [PATCH 127/374] adapt test outcomes --- .../CollectionOwnershipBasicTyping.java | 14 +++++-- .../CollectionOwnershipDefaults.java | 41 +++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 767daa87bbe6..8ca904bbe54e 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -35,6 +35,8 @@ Collection checkReturn(List list) { } void testAssignmentTransfersOwnership() { + // col is overwritten and its obligation never fulfilled or passed on + // :: error: unfulfilled.collection.obligations Collection col = new ArrayList<>(); Collection col2 = new ArrayList<>(); // col : @OwningCollection, col2 : @OwningCollection @@ -45,20 +47,21 @@ void testAssignmentTransfersOwnership() { checkArgIsOwning(col); // :: error: argument checkArgIsOwning(col2); - - closeElements(col); } /* - * Check that a resource collection constructed without explicit type argument also is of type @OwningCollection. + * Check that a resource collection constructed without explicit type argument is of type @OwningCollection + * as well. */ void testDiamond() { Collection col = new ArrayList<>(); - // :: error: arg + // :: error: argument checkArgIsOCwoO(col); closeElements(col); } + // TODO remove once fulfillment works + // :: error: unfulfilled.collection.obligations void closeElements(@OwningCollection Collection socketCollection) { for (Socket s : socketCollection) { try { @@ -68,7 +71,10 @@ void closeElements(@OwningCollection Collection socketCollection) { } } + // these two methods take on unfulfillable obligations and thus throw an error + void checkArgIsOwning( + // :: error: unfulfilled.collection.obligations @OwningCollection Collection collection) {} void checkArgIsOCwoO( diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index b00f958238f5..04a87442aed2 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -63,7 +63,7 @@ void checkParamIsNotOwningCollection(List list) { } /* - * Return type should default to @OwningCollection. + * Return type should default to @OwningCollection. Thus this is perfectly okay. */ List identity(@OwningCollection List list) { return list; @@ -78,6 +78,9 @@ List overrideReturnType(List list) { } void overrideReturnTypeClient() { + // the arraylist passed to the method is never closed. The ownership is not passed + // to overrideReturnType(), since parameters are @NotOwningCollection by default + // :: error: unfulfilled.collection.obligations List notOwninglist = overrideReturnType(new ArrayList()); List owninglist = identity(new ArrayList()); @@ -91,8 +94,8 @@ void overrideReturnTypeClient() { * to @OwningCollection). I.e. check that the resource collection parameter default is visible at call-site as well. */ void checkResourceCollectionParameterDefault() { - @OwningCollection List list = new ArrayList(); - identity(list); + List list = new ArrayList(); + closeElements(identity(list)); } /* @@ -111,10 +114,10 @@ void checkResourceCollectionReturnValueDefault() { * Checks that a newly allocated resource collection has type @OwningCollection. */ void checkNewResourceCollectionDefault() { - // List newResourceCollection = new ArrayList(); - // checkArgIsOwning(newResourceCollection); - // // :: error: argument - // checkArgIsOCwoO(newResourceCollection); + List newResourceCollection = new ArrayList(); + checkArgIsOwning(newResourceCollection); + // :: error: argument + checkArgIsOCwoO(newResourceCollection); Socket[] newResourceArray = new Socket[n]; checkArgIsOwning(newResourceArray); @@ -122,9 +125,33 @@ void checkNewResourceCollectionDefault() { checkArgIsOCwoO(newResourceArray); } + // TODO remove once fulfillment works + // :: error: unfulfilled.collection.obligations + void closeElements(Socket @OwningCollection [] socketCollection) { + for (Socket s : socketCollection) { + try { + s.close(); + } catch (Exception e) { + } + } + } + + // TODO remove once fulfillment works + // :: error: unfulfilled.collection.obligations + void closeElements(@OwningCollection Collection socketCollection) { + for (Socket s : socketCollection) { + try { + s.close(); + } catch (Exception e) { + } + } + } + void checkArgIsOwning( + // :: error: unfulfilled.collection.obligations @OwningCollection Collection collection) {} + // :: error: unfulfilled.collection.obligations void checkArgIsOwning(Socket @OwningCollection [] collection) {} void checkArgIsOCwoO( From 4caca982db53d9dd4a6dc90aa3e73b659a88ce79 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 19 Jun 2025 16:44:09 +0200 Subject: [PATCH 128/374] suppress rc warnings in qualifierhierarchy --- .../checkerframework/framework/type/QualifierHierarchy.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java index e4c90b057079..d26e216e1367 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java @@ -779,6 +779,10 @@ public static void assertSameSize(Collection c1, Collection c2) { * @param c2 the second collection * @param result the result collection */ + // these parameters could accept any resource collection. By default, they are not owning, which + // means they don't have an obligation for calling methods on their elements, but that also means + // that they cannot be passed as argument to a paramter expecting bottom. This is harmless however. + @SuppressWarnings("collectionownership:argument") public static void assertSameSize( @MustCallUnknown Collection c1, @MustCallUnknown Collection c2, From f9e3c8af710d0a2d8fc43e80a8950968dde2062e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 13:49:48 +0200 Subject: [PATCH 129/374] remove warning suppression, it is fixed otherwise --- .../checkerframework/framework/type/QualifierHierarchy.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java index d26e216e1367..e4c90b057079 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java @@ -779,10 +779,6 @@ public static void assertSameSize(Collection c1, Collection c2) { * @param c2 the second collection * @param result the result collection */ - // these parameters could accept any resource collection. By default, they are not owning, which - // means they don't have an obligation for calling methods on their elements, but that also means - // that they cannot be passed as argument to a paramter expecting bottom. This is harmless however. - @SuppressWarnings("collectionownership:argument") public static void assertSameSize( @MustCallUnknown Collection c1, @MustCallUnknown Collection c2, From 5bf62439b17573a39c067f7924815abb6ad05b7f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 13:52:18 +0200 Subject: [PATCH 130/374] wrap call to store.getValue in try-catch --- .../CollectionOwnershipTransfer.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 8090b3510898..605247e08abd 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -56,14 +56,10 @@ public TransferResult visitAssignment( TransferResult res = super.visitAssignment(node, in); CFStore store = res.getRegularStore(); - // Node lhs = node.getTarget(); - // lhs = getNodeOrTempVar(lhs); Node rhs = node.getExpression(); rhs = getNodeOrTempVar(rhs); - // JavaExpression lhsJx = JavaExpression.fromNode(lhs); JavaExpression rhsJx = JavaExpression.fromNode(rhs); - // CollectionOwnershipType lhsType = atypeFactory.getCoType(store.getValue(lhsJx)); CFValue rhsValue = null; try { rhsValue = store.getValue(rhsJx); @@ -212,14 +208,22 @@ public TransferResult visitObjectCreation( // is a resource collection, as it runs before the type variable is inferred: // List = new ArrayList<>(); // Thus, the following checks object creation expressions again on whether they are - // resource collections with no type variables, and if they are Bottom, they are + // resource collections with no type variables, and if they are @Bottom, they are // unrefined to @OwningCollection. Change the type of both the type var and the computed // expression itself. CFStore store = result.getRegularStore(); CFValue resultValue = result.getResultValue(); Node tempVarNode = getNodeOrTempVar(node); JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); - CollectionOwnershipType resolvedType = atypeFactory.getCoType(store.getValue(tempVarJx)); + + CFValue tempVarVal = null; + try { + tempVarVal = store.getValue(tempVarJx); + } catch (Exception e) { + return result; + } + + CollectionOwnershipType resolvedType = atypeFactory.getCoType(tempVarVal); TypeMirror javaTypeOfExpr = TreeUtils.elementFromTree(tempVarNode.getTree()).asType(); if (atypeFactory.isResourceCollection(javaTypeOfExpr)) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; From 2d903bf942f08c6fc1680a51a24be6056b0127a1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 13:53:43 +0200 Subject: [PATCH 131/374] consider iterables as collections as well --- .../checker/resourceleak/ResourceLeakUtils.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 556e5d3a3a80..ce9176e45ed1 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -288,7 +287,7 @@ public static boolean isCollection(TypeMirror type) { if (type == null) return false; Class elementRawType = TypesUtils.getClassFromType(type); if (elementRawType == null) return false; - return Collection.class.isAssignableFrom(elementRawType); + return Iterable.class.isAssignableFrom(elementRawType); } /** From 457577dd0c7ad31ecd86578b3fb49b76ce64bc28 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 14:07:39 +0200 Subject: [PATCH 132/374] fix interning error --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 57189d1e718f..b7e72ec25763 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2518,7 +2518,7 @@ private void checkMustCall( checker.reportError( firstAlias.tree, "unfulfilled.collection.obligations", - methodName == CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME + methodName.equals(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME) ? "Unknown" : methodName, firstAlias.stringForErrorMessage(), From 0be8bf83e07134ce4ff52877fd7a08edc8ab4e21 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 14:18:57 +0200 Subject: [PATCH 133/374] add missing documentation --- ...llectionOwnershipAnnotatedTypeFactory.java | 24 +++++++++++++++++++ .../MustCallConsistencyAnalyzer.java | 7 ++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 84e016350d9f..ad6f7a2a754c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -64,11 +64,21 @@ public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFa */ public final AnnotationMirror BOTTOM; + /** + * Enum for the types in the hierarchy. Combined with a few utility methods to get the right enum + * value from various sources, this is a convenient interface to deal with annotations in this + * hierarchy. + */ public enum CollectionOwnershipType { + /** the @NotOwningCollection type */ NotOwningCollection, + /** the @OwningCollection type */ OwningCollection, + /** the @OwningCollectionWithoutObligation type */ OwningCollectionWithoutObligation, + /** the @OwningCollectionBottom type */ OwningCollectionBottom, + /** if no type in the hierarchy can be determined with certainty */ None }; @@ -205,10 +215,24 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) } } + /** + * Utility method to get the {@code CollectionOwnershipType} that the given value extracted from a + * store has. + * + * @param val the store value type + * @return the {@code CollectionOwnershipType} that the given value extracted from a store has + */ public CollectionOwnershipType getCoType(CFValue val) { return val == null ? CollectionOwnershipType.None : getCoType(val.getAnnotations()); } + /** + * Utility method to extract the {@code CollectionOwnershipType} from a collection of {@code + * AnnotationMirror}s. + * + * @param annos the {@code AnnotationMirror} collection + * @return the extracted {@code CollectionOwnershipType} from annos + */ public CollectionOwnershipType getCoType(Collection annos) { if (annos == null) { return CollectionOwnershipType.None; diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index b7e72ec25763..b8ef779b8f22 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -271,6 +271,7 @@ public Obligation(Set resourceAliases, Set whenTo * right (sub)class. * * @param resourceAliases set of resource aliases for the new obligation + * @param whenToEnforce when this Obligation should be enforced * @return a new Obligation with the passed traits */ public Obligation getReplacement( @@ -493,14 +494,16 @@ public int hashCode() { } } - /** Obligation for a collection. To be fulfilled on its elements. */ + /** Obligation for calling a certain method on all elements of a collection. */ static class CollectionObligation extends Obligation { + /** The method that must be called on all elements of the collection. */ public String mustCallMethod; /** * Create a CollectionObligation from a set of resource aliases. * + * @param mustCallMethod the method to be called on all elements of the collection. * @param resourceAliases a set of resource aliases * @param whenToEnforce when this Obligation should be enforced */ @@ -733,7 +736,7 @@ public void analyze(ControlFlowGraph cfg) { * Adds {@code CollectionObligation}s if the return type is {@code @OwningCollection}. * * @param obligations the set of tracked obligations - * @node the node the check + * @param node the node of the return expression */ private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); From 0355af26cb9d7ec28a1eb6bbf38f22cfa1b6522d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 14:33:39 +0200 Subject: [PATCH 134/374] remove deprecated comment --- .../org/checkerframework/common/value/ValueCheckerUtils.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java index c9eb7bafa03a..2ec5c5f99a2f 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java @@ -178,11 +178,6 @@ private static T convertLongToType(long value, Class expectedType) { if (origValues == null) { return null; } - // this line is unsafe, which is why the warning suppression is required. - // `origValues` potentially carries elements with MustCall obligations, - // but the second parameter of mapList is not annotated to do so. - // It would have to have the same List - // annotation. return CollectionsPlume.mapList(Object::toString, origValues); } From 384fba13e6bd9f54ad070d57d96b7986b281c692 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 15:42:48 +0200 Subject: [PATCH 135/374] check @NotOwningCollection returns --- .../MustCallConsistencyAnalyzer.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index b8ef779b8f22..ab5a22ffbf6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2120,6 +2120,7 @@ private void propagateObligationsToSuccessorBlocks( } propagateObligationsToSuccessorBlock( + cfg, obligations, currentBlock, successorAndExceptionType.first, @@ -2133,6 +2134,7 @@ private void propagateObligationsToSuccessorBlocks( * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, * Deque)} that propagates obligations along a single edge. * + * @param cfg the control flow graph * @param obligations the Obligations for the current block * @param currentBlock the current block * @param successor a successor of the current block @@ -2143,6 +2145,7 @@ private void propagateObligationsToSuccessorBlocks( * @param worklist current worklist */ private void propagateObligationsToSuccessorBlock( + ControlFlowGraph cfg, Set obligations, Block currentBlock, Block successor, @@ -2306,6 +2309,20 @@ private void propagateObligationsToSuccessorBlock( exceptionType == null ? MethodExitKind.NORMAL_RETURN : MethodExitKind.EXCEPTIONAL_EXIT; if (obligation.whenToEnforce.contains(exitKind)) { checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); + } else if (exitKind.equals(MethodExitKind.NORMAL_RETURN)) { + // check normal returns where return type is @NotOwningCollection. + UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); + if (underlyingAST instanceof UnderlyingAST.CFGMethod) { + MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); + ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); + boolean returnTypeHasManualNocAnno = + coAtf.getCoType( + new HashSet<>(executableElement.getReturnType().getAnnotationMirrors())) + == CollectionOwnershipType.NotOwningCollection; + if (returnTypeHasManualNocAnno) { + checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); + } + } } } else { // In this case, there is info in the successor store about some alias in the From 34283fde6e3223434a9568b2b04c2e1968255335 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 15:43:05 +0200 Subject: [PATCH 136/374] add tests for return to @NotOwningCollection --- .../CollectionOwnershipBasicTyping.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 8ca904bbe54e..ee6b261a4bdb 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -34,6 +34,25 @@ Collection checkReturn(List list) { return list; } + // this return looks harmless. However, it returns a collection, but not its ownership. + // Since no obligation will be created at call-site (it expects a non-owning collection), + // this method must ensure the returned collection has no obligations, which it fails to + // do in this case. + @NotOwningCollection + Collection checkIllegalNotOwningReturn() { + // :: error: unfulfilled.collection.obligations + return new ArrayList<>(); + } + + // this is the correct version of above. It passes ownership to another method first, + // and then returns the non-owning reference. + @NotOwningCollection + Collection checkLegalNotOwningReturn() { + List list = new ArrayList<>(); + closeElements(list); + return list; + } + void testAssignmentTransfersOwnership() { // col is overwritten and its obligation never fulfilled or passed on // :: error: unfulfilled.collection.obligations From 02b6d942e3e0e03aec13f3a000dfd737390a5817 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 15:50:57 +0200 Subject: [PATCH 137/374] add unrefinement test for collection ownership type system --- .../CollectionOwnershipBasicTyping.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index ee6b261a4bdb..194122c8ffeb 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -53,6 +53,16 @@ Collection checkLegalNotOwningReturn() { return list; } + // check that unrefinement in assignments is allowed. + void checkUnrefinement() { + List list = new ArrayList<>(); + List newOwner = list; + // newOwner : @OwningCollection, list: @NotOwningCollection + list = newOwner; + // newOwner = @NotOwningCollection, list: @OwningCollection + closeElements(list); + } + void testAssignmentTransfersOwnership() { // col is overwritten and its obligation never fulfilled or passed on // :: error: unfulfilled.collection.obligations From 98c831b7ddb81c5f50a213ef0be90e536b63882f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 20 Jun 2025 16:19:08 +0200 Subject: [PATCH 138/374] replace unnecessary .equals with == --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index ab5a22ffbf6a..c81d8f66fc35 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2309,7 +2309,7 @@ private void propagateObligationsToSuccessorBlock( exceptionType == null ? MethodExitKind.NORMAL_RETURN : MethodExitKind.EXCEPTIONAL_EXIT; if (obligation.whenToEnforce.contains(exitKind)) { checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); - } else if (exitKind.equals(MethodExitKind.NORMAL_RETURN)) { + } else if (exitKind == MethodExitKind.NORMAL_RETURN) { // check normal returns where return type is @NotOwningCollection. UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); if (underlyingAST instanceof UnderlyingAST.CFGMethod) { From a11f29ac6ab3f825c3378273a0859c87932e4e77 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 21 Jun 2025 12:16:53 +0200 Subject: [PATCH 139/374] add utility method: is MethodInvocationTree call to hasNext()? --- .../java/org/checkerframework/javacutil/TreeUtils.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index afd4e815a4a4..e37c614a60a3 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -166,6 +166,16 @@ private TreeUtils() { public static boolean isConstructor(MethodTree tree) { return tree.getName().contentEquals(""); } + + /** + * Checks if the method invocation is a call to hasNext. + * + * @param tree a tree defining a method invocation + * @return true iff tree describes a call to hasNext + */ + public static boolean isHasNextCall(MethodInvocationTree tree) { + return isNamedMethodCall("hasNext", tree); + } /** * Checks if the method invocation is a call to super. From 17e413dbeaf2222e75dd946f4c7ff875bcdaa641 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 21 Jun 2025 12:17:06 +0200 Subject: [PATCH 140/374] make getCalledMethods in CmAtf public --- .../calledmethods/CalledMethodsAnnotatedTypeFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java index 4debd68b0a0f..8edc0282e280 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java @@ -407,7 +407,7 @@ protected CalledMethodsAnalysis createFlowAnalysis() { * @param calledMethodsAnnotation the annotation * @return the called methods */ - protected List getCalledMethods(AnnotationMirror calledMethodsAnnotation) { + public List getCalledMethods(AnnotationMirror calledMethodsAnnotation) { return AnnotationUtils.getElementValueArray( calledMethodsAnnotation, calledMethodsValueElement, String.class, Collections.emptyList()); } From 987f30cb87bc4052b94b13307feb1ef351efc3a7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 21 Jun 2025 12:18:49 +0200 Subject: [PATCH 141/374] add utility method to rlccmatf to get store after block --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index bd809761e536..d011d5faf6a1 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -370,6 +370,18 @@ public boolean canCreateObligations() { return !rlc.hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); } + /** + * Fetches the store from the results of dataflow for {@code block}. The store after {@code block} + * is returned. + * + * @param block a block + * @return the appropriate CFStore, populated with CalledMethods annotations, from the results of + * running dataflow + */ + public AccumulationStore getStoreAfterBlock(Block block) { + return flowResult.getStoreAfter(block); + } + @Override @SuppressWarnings("TypeParameterUnusedInFormals") // Intentional abuse public > From 0ab52aae8cea2d88f6b0eb25d784c10f388d47dc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 21 Jun 2025 16:18:38 +0200 Subject: [PATCH 142/374] add loop body analysis --- ...llectionOwnershipAnnotatedTypeFactory.java | 5 + .../MustCallConsistencyAnalyzer.java | 569 +++++++++++++++++- .../RLCCalledMethodsAnnotatedTypeFactory.java | 185 ++++++ .../expression/IteratedCollectionElement.java | 127 ++++ 4 files changed, 857 insertions(+), 29 deletions(-) create mode 100644 dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ad6f7a2a754c..ac03fd314785 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -88,6 +89,10 @@ public enum CollectionOwnershipType { */ public static final String UNKNOWN_METHOD_NAME = "1UNKNOWN"; + public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { + // TODO SCK: fill this out + } + /** * Creates a CollectionOwnershipAnnotatedTypeFactory. * diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index c81d8f66fc35..fdb8ad17d16a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -4,6 +4,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -50,9 +51,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnalysis; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsVisitor; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; +import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.UnderlyingAST.Kind; @@ -60,9 +63,12 @@ import org.checkerframework.dataflow.cfg.block.Block.BlockType; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.ClassNameNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.LessThanNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -71,7 +77,9 @@ import org.checkerframework.dataflow.cfg.node.ReturnNode; import org.checkerframework.dataflow.cfg.node.SuperNode; import org.checkerframework.dataflow.cfg.node.ThisNode; +import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode; import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.ThisReference; @@ -352,25 +360,25 @@ private boolean canBeSatisfiedThrough(LocalVariableNode localVariableNode) { return getResourceAlias(localVariableNode) != null; } - // /** - // * Returns true if this contains a resource alias corresponding to {@code localVariableNode}, - // * meaning that calling the required methods on {@code localVariableNode} is sufficient to - // * satisfy the must-call obligation this object represents. - // * - // * @param tree a local variable tree - // * @return true if a resource alias corresponding to {@code tree} is present - // */ - // private boolean canBeSatisfiedThrough(Tree tree) { - // for (ResourceAlias alias : resourceAliases) { - // if (alias.tree.equals(tree) - // || ((tree instanceof ExpressionTree) - // && JavaExpression.fromTree((ExpressionTree) tree) != null - // && alias.reference.equals(JavaExpression.fromTree((ExpressionTree) tree)))) { - // return true; - // } - // } - // return false; - // } + /** + * Returns true if this contains a resource alias corresponding to {@code localVariableNode}, + * meaning that calling the required methods on {@code localVariableNode} is sufficient to + * satisfy the must-call obligation this object represents. + * + * @param tree a local variable tree + * @return true if a resource alias corresponding to {@code tree} is present + */ + private boolean canBeSatisfiedThrough(Tree tree) { + for (ResourceAlias alias : resourceAliases) { + if (alias.tree.equals(tree) + || ((tree instanceof ExpressionTree) + && JavaExpression.fromTree((ExpressionTree) tree) != null + && alias.reference.equals(JavaExpression.fromTree((ExpressionTree) tree)))) { + return true; + } + } + return false; + } /** * Does this Obligation contain any resource aliases that were derived from {@link @@ -2119,14 +2127,19 @@ private void propagateObligationsToSuccessorBlocks( // tracking. } - propagateObligationsToSuccessorBlock( - cfg, - obligations, - currentBlock, - successorAndExceptionType.first, - successorAndExceptionType.second, - visited, - worklist); + try { + propagateObligationsToSuccessorBlock( + false, + cfg, + obligations, + currentBlock, + successorAndExceptionType.first, + successorAndExceptionType.second, + visited, + worklist); + } catch (InvalidLoopBodyAnalysisException e) { + // this exception is only thrown from the loop body analysis, which has another entry point. + } } } @@ -2134,6 +2147,8 @@ private void propagateObligationsToSuccessorBlocks( * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, * Deque)} that propagates obligations along a single edge. * + * @param isLoopBodyAnalysis true if part of a loop body analysis (instead of consistency + * analysis) * @param cfg the control flow graph * @param obligations the Obligations for the current block * @param currentBlock the current block @@ -2145,13 +2160,15 @@ private void propagateObligationsToSuccessorBlocks( * @param worklist current worklist */ private void propagateObligationsToSuccessorBlock( + boolean isLoopBodyAnalysis, ControlFlowGraph cfg, Set obligations, Block currentBlock, Block successor, @Nullable TypeMirror exceptionType, Set visited, - Deque worklist) { + Deque worklist) + throws InvalidLoopBodyAnalysisException { List currentBlockNodes = currentBlock.getNodes(); // successorObligations eventually contains the Obligations to propagate to successor. // The loop below mutates it. @@ -2171,8 +2188,30 @@ private void propagateObligationsToSuccessorBlock( + ((ExceptionBlock) currentBlock).getNode().getTree() + " with exception type " + exceptionType; + TransferInput input = cmAtf.getInput(successor); + if (input == null) { + if (isLoopBodyAnalysis) { + // there are CFG nodes that have no incoming store. In the consistency analysis, + // these are not explored. However, in the loop body analysis, such a node may be explicitly + // explored + // (if a potentially fulfilling loop is in an unreachable block). Hence it is safe to return + // here and + // not propagate obligations, since the state is not reached by the analysis anyways. + // Not just that, but note that if this state is reached, the entire loop is in an state + // unreachable + // by the analysis. If the beginning of the loop was reachable, the analysis wouldn't have + // taken a path that is + // unreachable for it. Hence, the entire loop body analysis can be aborted here. + // The thrown exception is caught in the caller and the loop body analysis aborts, i.e. + // returns + // immediately. + throw new InvalidLoopBodyAnalysisException("Block with no incoming store."); + } else { + throw new BugInCF("block with no outgoing incoming store: " + successor); + } + } // Computed outside the Obligation loop for efficiency. - AccumulationStore regularStoreOfSuccessor = cmAtf.getInput(successor).getRegularStore(); + AccumulationStore regularStoreOfSuccessor = input.getRegularStore(); for (Obligation obligation : obligations) { // This boolean is true if there is no evidence that the Obligation does not go out // of scope - that is, if there is definitely a resource alias that is in scope in @@ -2495,6 +2534,25 @@ private static boolean varTrackedInObligations( return null; } + /** + * Gets the Obligation whose resource alias set contains the given tree, if one exists in {@code + * obligations}. + * + * @param obligations a set of Obligations + * @param tree variable tree of interest + * @return the Obligation in {@code obligations} whose resource alias set contains {@code tree}, + * or {@code null} if there is no such Obligation + */ + /*package-private*/ static @Nullable Obligation getObligationForVar( + Set obligations, Tree tree) { + for (Obligation obligation : obligations) { + if (obligation.canBeSatisfiedThrough(tree)) { + return obligation; + } + } + return null; + } + /** * Gets the set of Obligations whose resource alias set contain the given local variable, or an * empty set if none exist. in {@code obligations}. @@ -2816,4 +2874,457 @@ public static String collectionToString(Collection bwos) { return result.toString(); } } + + /* + * SECTION: Loop Body Analysis. This section finds loops and analyzes them to determine whether they + * call methods on each element of a collection, which allows for fulfilling collection obligations. + * It reuses much of the cfg traversal logic of the consistency analysis, but is it's own separate + * thing. + */ + + /** + * Traverses the cfg of a method to find and mark enhanced-for-loops that potentially fulfill + * {@code CollectionObligation}s. + * + * @param cfg the cfg of the method to analyze + */ + public void findFulfillingForEachLoops(ControlFlowGraph cfg) { + // The `visited` set contains everything that has been added to the worklist, even if it has + // not yet been removed and analyzed. + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + + // Add any owning parameters to the initial set of variables to track. + BlockWithObligations entry = + new BlockWithObligations(cfg.getEntryBlock(), new HashSet()); + worklist.add(entry); + visited.add(entry); + + while (!worklist.isEmpty()) { + BlockWithObligations current = worklist.remove(); + Block currentBlock = current.block; + + for (IPair successorAndExceptionType : + getSuccessorsExceptIgnoredExceptions(currentBlock)) { + for (Node node : currentBlock.getNodes()) { + if (node instanceof MethodInvocationNode) { + patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg); + } else if (node instanceof ArrayAccessNode) { + patternMatchEnhancedArrayForLoop((ArrayAccessNode) node, cfg); + } + } + propagate( + new BlockWithObligations(successorAndExceptionType.first, new HashSet()), + visited, + worklist); + } + } + } + + /** + * Checks whether the given {@code MethodInvocationNode} is desugared from an enhanced for loop + * and calls a loop-body-analysis on the detected loop if it is. + * + *

If a {@code MethodInvocationNode} is desugared from an enhanced for loop over a collection + * it corresponds to the node in the synthetic {@code Iterator.next()} method call, which is the + * loop update instruction. The AST node corresponding to the loop itself is in this case + * contained as a field in the {@code MethodInvocationNode}, which is set in the CFG translation + * phase one. + * + *

This method now traverses the CFG upwards to find the loop condition and downwards to find + * the first block of the loop body. With these two blocks, it can then call a loop-body-analysis + * to find the methods the loop calls on the elements of the iterated collection, as part of the + * MustCallOnElements checker. + * + * @param methodInvocationNode the {@code MethodInvocationNode}, for which it is checked, whether + * it is desugared from an enhanced for loop. + * @param cfg the enclosing cfg of the {@code MethodInvocationNode} + */ + private void patternMatchEnhancedCollectionForLoop( + MethodInvocationNode methodInvocationNode, ControlFlowGraph cfg) { + boolean nodeIsDesugaredFromEnhancedForLoop = + methodInvocationNode.getIterableExpression() != null; + if (nodeIsDesugaredFromEnhancedForLoop) { + // this is the Iterator.next() call desugared from an enhanced-for-loop + EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); + if (loop == null) { + throw new BugInCF( + "MethodInvocationNode.iterableExpression should be non-null iff" + + " MethodInvocationNode.enhancedForLoop is non-null"); + } + + // Find the first block of the loop body. + // Start from the synthetic (desugared) iterator.next() node and traverse the cfg + // until the assignment of the loop iterator variable is found, which is the last + // desugared instruction. The next block is then the start of the loop body. + VariableTree loopVariable = loop.getVariable(); + SingleSuccessorBlock ssblock = (SingleSuccessorBlock) methodInvocationNode.getBlock(); + Iterator nodeIterator = ssblock.getNodes().iterator(); + Node loopVarNode = null; + Node node; + boolean isAssignmentOfIterVar; + do { + while (!nodeIterator.hasNext()) { + ssblock = (SingleSuccessorBlock) ssblock.getSuccessor(); + nodeIterator = ssblock.getNodes().iterator(); + } + node = nodeIterator.next(); + isAssignmentOfIterVar = false; + if ((node instanceof AssignmentNode) && node.getTree().getKind() == Tree.Kind.VARIABLE) { + loopVarNode = ((AssignmentNode) node).getTarget(); + VariableTree iterVarDecl = (VariableTree) node.getTree(); + isAssignmentOfIterVar = iterVarDecl.getName() == loopVariable.getName(); + } + } while (!isAssignmentOfIterVar); + Block loopBodyEntryBlock = ssblock.getSuccessor(); + + // Find the loop-body-condition + // Start from the synthetic (desugared) iterator.next() node and traverse the cfg + // backwards until the conditional block is found. The previous block is then the block + // containing the desugared loop condition iterator.hasNext(). + Block block = methodInvocationNode.getBlock(); + nodeIterator = block.getNodes().iterator(); + boolean isLoopCondition; + do { + while (!nodeIterator.hasNext()) { + Set predBlocks = block.getPredecessors(); + if (predBlocks.size() == 1) { + block = predBlocks.iterator().next(); + nodeIterator = block.getNodes().iterator(); + } else { + System.out.println("predecessor: " + predBlocks); + throw new BugInCF( + "Encountered more than one CFG Block predeccessor trying to find the" + + " enhanced-for-loop update block. Block: "); + // + block + // + "\nPredecessors: " + // + predBlocks); + } + } + node = nodeIterator.next(); + isLoopCondition = false; + if (node instanceof MethodInvocationNode) { + MethodInvocationTree mit = ((MethodInvocationNode) node).getTree(); + isLoopCondition = TreeUtils.isHasNextCall(mit); + } + } while (!isLoopCondition); + + // add the blocks into a static datastructure in the calledmethodsatf, such that it can + // analyze + // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees + // to the static datastructure in McoeAtf) + PotentiallyFulfillingLoop pfLoop = + new PotentiallyFulfillingLoop( + loop.getExpression(), + loopVarNode.getTree(), + node.getTree(), + loopBodyEntryBlock, + block, + loopVarNode); + this.analyzeObligationFulfillingLoop(cfg, pfLoop); + } + } + + /** + * Checks whether the given {@code ArrayAccessNode} is desugared from an enhanced for loop and + * calls a loop-body-analysis on the detected loop if it is. + * + *

If an {@code ArrayAccessNode} is desugared from an enhanced for loop over an array, it + * corresponds to the node in the synthetic {@code s = array#numX[index#numY]} assignment, where + * the loop iterator variable is assigned. The AST node corresponding to the loop itself is in + * this case contained as a field in the {@code ArrayAccessNode}, which is set in the CFG + * translation phase one. + * + *

This method now traverses the CFG upwards to find the loop condition and downwards to find + * the first block of the loop body. With these two blocks, it can then call a loop-body-analysis + * to find the methods the loop calls on the elements of the iterated collection, as part of the + * MustCallOnElements checker. + * + * @param arrayAccessNode the {@code ArrayAccessNode}, for which it is checked, whether it is + * desugared from an enhanced for loop. + * @param cfg the enclosing cfg of the {@code ArrayAccessNode} + */ + private void patternMatchEnhancedArrayForLoop( + ArrayAccessNode arrayAccessNode, ControlFlowGraph cfg) { + boolean nodeIsDesugaredFromEnhancedForLoop = arrayAccessNode.getArrayExpression() != null; + if (nodeIsDesugaredFromEnhancedForLoop && cfg != null) { + // this is the arr[i] access desugared from an enhanced-for-loop (in iter = arr[i];) + EnhancedForLoopTree loop = arrayAccessNode.getEnhancedForLoop(); + if (loop == null) { + throw new BugInCF( + "MethodInvocationNode.iterableExpression should be non-null iff" + + " MethodInvocationNode.enhancedForLoop is non-null"); + } + + // Find the first block of the loop body. + SingleSuccessorBlock ssblock = (SingleSuccessorBlock) arrayAccessNode.getBlock(); + Block loopBodyEntryBlock = ssblock.getSuccessor(); + + // Find the loop condition + // Start from the synthetic (desugared) arr[i] node and traverse the cfg + // backwards until the LessThan node is found. + // It corresponds to the desugared loop condition (index#numX < array#numX.length). + Block block = arrayAccessNode.getBlock(); + Iterator nodeIterator = block.getNodes().iterator(); + Node loopVarNode = null; + Node node; + do { + while (!nodeIterator.hasNext()) { + Set predBlocks = block.getPredecessors(); + if (predBlocks.size() == 1) { + block = predBlocks.iterator().next(); + nodeIterator = block.getNodes().iterator(); + } else { + throw new BugInCF( + "Encountered more than one CFG Block predeccessor trying to find the" + + " enhanced-for-loop update block."); + } + } + node = nodeIterator.next(); + if (node instanceof VariableDeclarationNode) { + // variable declaration of public iterator + loopVarNode = node; + } + } while (!(node instanceof LessThanNode)); + + // add the blocks into a static datastructure in the calledmethodsatf, such that it can + // analyze + // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees + // to the static datastructure in McoeAtf) + PotentiallyFulfillingLoop pfLoop = + new PotentiallyFulfillingLoop( + loop.getExpression(), + loopVarNode.getTree(), + node.getTree(), + loopBodyEntryBlock, + block, + loopVarNode); + this.analyzeObligationFulfillingLoop(cfg, pfLoop); + } + } + + /** + * Analyze the loop body of a 'potentially-mcoe-obligation-fulfilling-loop', as determined by a + * pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or determined by a + * pre-pattern-match in {@code this.patternMatchEnhancedForLoop(MethodInvocationNode)} (in the + * case of an enhanced-for-loop). + * + *

The analysis uses the CalledMethods type of the collection element iterated over to + * determine the methods the loop calls on the collection elements. + * + *

This method should be called after the called-method-analysis is finished (in the {@code + * postAnalyze(cfg)} method of the {@code RLCCalledMethodsAnnotatedTypeFactory}). + * + * @param cfg the cfg of the enclosing method + * @param potentiallyFulfillingLoop the loop to check + */ + public void analyzeObligationFulfillingLoop( + ControlFlowGraph cfg, PotentiallyFulfillingLoop potentiallyFulfillingLoop) { + + // ensure checked loop is initialized in a valid way + Objects.requireNonNull( + potentiallyFulfillingLoop.collectionElementTree, + "CollectionElementAccess tree provided to analyze loop body of an" + + " mcoe-obligation-fulfilling loop is null."); + Objects.requireNonNull( + potentiallyFulfillingLoop.loopBodyEntryBlock, + "Block provided to analyze loop body of an mcoe-obligation-fulfilling loop is null."); + Objects.requireNonNull( + potentiallyFulfillingLoop.loopUpdateBlock, + "Block provided to analyze loop body of an mcoe-obligation-fulfilling loop is null."); + + Block loopBodyEntryBlock = potentiallyFulfillingLoop.loopBodyEntryBlock; + Block loopUpdateBlock = potentiallyFulfillingLoop.loopUpdateBlock; + Tree collectionElement = potentiallyFulfillingLoop.collectionElementTree; + + boolean emptyLoopBody = loopBodyEntryBlock == loopUpdateBlock; + if (emptyLoopBody) { + return; + } + + // The `visited` set contains everything that has been added to the worklist, even if it has + // not yet been removed and analyzed. + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + + // Add an obligation for the element of the collection iterated over + + Obligation collectionElementObligation = Obligation.fromTree(collectionElement); + if (collectionElement.getKind() == Tree.Kind.VARIABLE) { + VariableElement varElt = TreeUtils.elementFromDeclaration((VariableTree) collectionElement); + boolean hasMustCallAlias = cmAtf.hasMustCallAlias(varElt); + collectionElementObligation = + new Obligation( + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(varElt), varElt, collectionElement, hasMustCallAlias)), + Collections.singleton(MethodExitKind.NORMAL_RETURN)); + } + + BlockWithObligations loopBodyEntry = + new BlockWithObligations( + loopBodyEntryBlock, Collections.singleton(collectionElementObligation)); + + worklist.add(loopBodyEntry); + visited.add(loopBodyEntry); + Set calledMethodsInLoop = null; + + // main loop: propagate obligations block-by-block + while (!worklist.isEmpty()) { + BlockWithObligations current = worklist.remove(); + Block currentBlock = current.block; + + for (IPair successorAndExceptionType : + getSuccessorsExceptIgnoredExceptions(currentBlock)) { + Set obligations = new LinkedHashSet<>(current.obligations); + for (Node node : currentBlock.getNodes()) { + if (node instanceof AssignmentNode) { + updateObligationsForAssignment(obligations, cfg, (AssignmentNode) node); + } + } + + @SuppressWarnings("interning:not.interned") + boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock; + if (isLastBlockOfBody) { + Set calledMethodsAfterBlock = + analyzeTypeOfCollectionElement(currentBlock, potentiallyFulfillingLoop, obligations); + // intersect the called methods after this block with the accumulated ones so far. + // This is required because there may be multiple "back edges" of the loop, in which + // case we must intersect the called methods between those. + if (calledMethodsInLoop == null) { + calledMethodsInLoop = calledMethodsAfterBlock; + } else { + calledMethodsInLoop.retainAll(calledMethodsAfterBlock); + } + } else { + try { + propagateObligationsToSuccessorBlock( + true, + cfg, + obligations, + currentBlock, + successorAndExceptionType.first, + successorAndExceptionType.second, + visited, + worklist); + } catch (InvalidLoopBodyAnalysisException e) { + return; + } + } + } + } + + System.out.println("calledmethodsinloop: " + calledMethodsInLoop); + // now put the loop into the static datastructure if it calls any methods on the element + if (calledMethodsInLoop != null && calledMethodsInLoop.size() > 0) { + potentiallyFulfillingLoop.addMethods(calledMethodsInLoop); + CollectionOwnershipAnnotatedTypeFactory.markFulfillingLoop(potentiallyFulfillingLoop); + } + } + + /** + * Checks the CalledMethods store after the given block and determines the CalledMethods type of + * the given tree (which corresponds to the collection element iterated over) and returns the + * union of methods in the CalledMethods type of the collection element and all its resource + * aliases. + * + * @param lastLoopBodyBlock last block of loop body + * @param potentiallyFulfillingLoop loop wrapper of the loop to analyze + * @param obligations the set of tracked obligations + * @return the union of methods in the CalledMethods type of the collection element and all its + * resource aliases. + */ + private Set analyzeTypeOfCollectionElement( + Block lastLoopBodyBlock, + PotentiallyFulfillingLoop potentiallyFulfillingLoop, + Set obligations) { + AccumulationStore store = null; + if (lastLoopBodyBlock.getLastNode() == null) { + // TODO is this really the right store? I think we need to get the then-or else store + store = cmAtf.getStoreAfterBlock(lastLoopBodyBlock); + } else { + store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); + } + Obligation collectionElementObligation = + getObligationForVar(obligations, potentiallyFulfillingLoop.collectionElementTree); + if (collectionElementObligation == null) { + // the loop did something weird. Might have reassigned the collection element. + // The sound thing to do is return an empty list. + System.out.println("obligation gone for collection element"); + return new HashSet<>(); + // throw new BugInCF( + // "No obligation for collection element " + // + potentiallyFulfillingLoop.collectionElementTree); + } + + Set calledMethodsAfterThisBlock = new HashSet<>(); + + // add the called methods of the ICE + IteratedCollectionElement ice = + store.getIteratedCollectionElement( + potentiallyFulfillingLoop.collectionElementNode, + potentiallyFulfillingLoop.collectionElementTree); + if (ice != null) { + AccumulationValue cmValOfIce = store.getValue(ice); + List calledMethods = getCalledMethods(cmValOfIce); + if (calledMethods != null && calledMethods.size() > 0) { + calledMethodsAfterThisBlock.addAll(calledMethods); + } + } + + // add the called methods of possible aliases of the collection element + for (ResourceAlias alias : collectionElementObligation.resourceAliases) { + AccumulationValue cmValOfAlias = store.getValue(alias.reference); + if (cmValOfAlias == null) continue; + List calledMethods = getCalledMethods(cmValOfAlias); + if (calledMethods != null && calledMethods.size() > 0) { + calledMethodsAfterThisBlock.addAll(calledMethods); + } + } + + return calledMethodsAfterThisBlock; + } + + /** + * Returns the set of called methods values given an AccumulationValue. + * + * @param cmVal the accumulation value + * @return the set of called methods of the given value + */ + private List getCalledMethods(AccumulationValue cmVal) { + Set calledMethods = cmVal.getAccumulatedValues(); + if (calledMethods != null) { + return new ArrayList<>(calledMethods); + } else { + for (AnnotationMirror anno : cmVal.getAnnotations()) { + if (AnnotationUtils.areSameByName( + anno, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { + return cmAtf.getCalledMethods(anno); + } + } + } + return new ArrayList<>(); + } + + /** + * If this exception is thrown, it indicates to the caller of the method that the loop body + * analysis should be aborted and immediately return. This happens if a {@code Block} is + * encountered, which does not have an incoming store, meaning the analysis is not supposed to + * reach it. However, in the loop body analysis, such a store may be explicitly explored (if a + * potentially fulfilling loop is in an unreachable {@code Block}). It is then impossible to + * proceed with the analysis, since stores for these {@code Block}s don't exist. Hence, the + * analysis should abort. + */ + @SuppressWarnings("serial") + private static class InvalidLoopBodyAnalysisException extends Exception { + + /** + * Construct an InvalidLoopBodyAnalysisException + * + * @param message the error message + */ + public InvalidLoopBodyAnalysisException(String message) { + super(message); + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index d011d5faf6a1..a263ebd99b78 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -2,10 +2,13 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; @@ -29,6 +32,7 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; @@ -552,4 +556,185 @@ public TransferInput getInput(Block block) return analysis.getInput(block); } } + + /** Wrapper class for a loop that might have an effect on the obligation of a collection/array. */ + public abstract static class CollectionObligationAlteringLoop { + /** Loop is either assigning or fulfilling. */ + public static enum LoopKind { + /** + * Loop potentially assigns elements with non-empty {@code @MustCall} type to a collection. + */ + ASSIGNING, + + /** Loop potentially calls methods on all elements of a collection. */ + FULFILLING + } + + /** AST {@code Tree} for collection iterated over. */ + public final ExpressionTree collectionTree; + + /** AST {@code Tree} for collection element iterated over. */ + public final Tree collectionElementTree; + + /** AST {@code Tree} for loop condition. */ + public final Tree condition; + + /** + * methods associated with this loop. For assigning loops, these are methods that are to be + * added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to be + * removed from the {@code MustCallOnElements} and added to the {@code CalledMethodsOnElements} + * type. + */ + protected final Set associatedMethods; + + /** + * Wether loop is assigning (elements with {@code MustCall} obligations to a collection) or + * fulfilling. + */ + public final LoopKind loopKind; + + /** + * Constructs a new {@code CollectionObligationAlteringLoop}. Called by subclass constructor. + * + * @param collectionTree AST {@code Tree} for collection iterated over + * @param collectionElementTree AST {@code Tree} for collection element iterated over + * @param condition AST {@code Tree} for loop condition + * @param associatedMethods set of methods associated with this loop + * @param loopKind whether this is an assigning/fulfilling loop + */ + protected CollectionObligationAlteringLoop( + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree condition, + Set associatedMethods, + LoopKind loopKind) { + this.collectionTree = collectionTree; + this.collectionElementTree = collectionElementTree; + this.condition = condition; + this.loopKind = loopKind; + if (associatedMethods == null) { + associatedMethods = new HashSet<>(); + } + this.associatedMethods = associatedMethods; + } + + /** + * Add methods associated with this loop. For assigning loops, these are methods that are to be + * added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to be + * removed from the {@code MustCallOnElements} and added to the {@code CalledMethodsOnElements} + * type. + * + * @param methods the set of methods to add + */ + public void addMethods(Set methods) { + associatedMethods.addAll(methods); + } + + /** + * Return methods associated with this loop. For assigning loops, these are methods that are to + * be added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to + * be removed from the {@code MustCallOnElements} and added to the {@code + * CalledMethodsOnElements} type. + * + * @return the set of associated methdos + */ + public Set getMethods() { + return associatedMethods; + } + } + + /** + * Wrapper for a loop that potentially assigns elements with non-empty {@code MustCall} + * obligations to an array, thus creating {@code MustCallOnElements} obligations for the array. + */ + public static class PotentiallyAssigningLoop extends CollectionObligationAlteringLoop { + /** The AST tree for the assignment of the resource into the array in the loop. */ + public final AssignmentTree assignment; + + /** + * Constructs a new {@code PotentiallyAssigningLoop} + * + * @param collectionTree AST {@code Tree} for collection iterated over + * @param collectionElementTree AST {@code Tree} for collection element iterated over + * @param condition AST {@code Tree} for loop condition + * @param assignment AST tree for the assignment of the resource into the array in the loop + * @param methodsToCall set of methods that are to be added to the {@code MustCallOnElements} + * type of the array iterated over. + */ + public PotentiallyAssigningLoop( + ExpressionTree collectionTree, + ExpressionTree collectionElementTree, + Tree condition, + AssignmentTree assignment, + Set methodsToCall) { + super( + collectionTree, + collectionElementTree, + condition, + Set.copyOf(methodsToCall), + CollectionObligationAlteringLoop.LoopKind.ASSIGNING); + this.assignment = assignment; + } + } + + /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ + public static class PotentiallyFulfillingLoop extends CollectionObligationAlteringLoop { + /** cfg {@code Block} for the loop body entry */ + public final Block loopBodyEntryBlock; + + /** cfg {@code Block} for the loop update */ + public final Block loopUpdateBlock; + + /** cfg {@code Node} for the collection element iterated over */ + public final Node collectionElementNode; + + /** + * Constructs a new {@code PotentiallyFulfillingLoop} + * + * @param collectionTree AST {@link Tree} for collection iterated over + * @param collectionElementTree AST {@link Tree} for collection element iterated over + * @param condition AST {@link Tree} for loop condition + * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry + * @param loopUpdateBlock cfg {@link Block} for the loop update + * @param collectionEltNode cfg {@link Node} for the collection element iterated over + */ + public PotentiallyFulfillingLoop( + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree condition, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + Node collectionEltNode) { + super( + collectionTree, + collectionElementTree, + condition, + new HashSet<>(), + CollectionObligationAlteringLoop.LoopKind.FULFILLING); + this.loopBodyEntryBlock = loopBodyEntryBlock; + this.loopUpdateBlock = loopUpdateBlock; + this.collectionElementNode = collectionEltNode; + } + } + + /** + * After running the called-methods analysis, call the consistency analyzer to analyze the loop + * bodys of 'potentially-collection-obligation-fulfilling-loops', as determined by a + * pre-pattern-match in the MustCallVisitor. + * + *

The analysis uses the CalledMethods type of the collection element iterated over to + * determine the methods the loop calls on the collection elements. + * + * @param cfg the cfg of the enclosing method + */ + @Override + public void postAnalyze(ControlFlowGraph cfg) { + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = + new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this)); + // since this runs before the consistency analysis, we unfortunately have to traverse the cfg + // twice. The first time to find these for-each loop, and the second time much later to do the + // final consistency analysis. + mustCallConsistencyAnalyzer.findFulfillingForEachLoops(cfg); + super.postAnalyze(cfg); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java new file mode 100644 index 000000000000..5a4e8824f357 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -0,0 +1,127 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.source.tree.Tree; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.AnnotationProvider; + +/** + * Represents a collection element that is iterated over in a potentially + * collection-obligation-fulfilling loop, for example {@code o} in {@code for (Object o: list) { }} + */ +public class IteratedCollectionElement extends JavaExpression { + /** The cfg node for this collection element. */ + public final Node node; + + /** The AST node for this collection element. */ + public final Tree tree; + + /** + * Creates a new IteratedCollectionElement. + * + * @param var a CFG node + * @param tree an AST tree + */ + public IteratedCollectionElement(Node var, Tree tree) { + super(var.getType()); + this.node = var; + this.tree = tree; + } + + @SuppressWarnings("interning:not.interned") + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof IteratedCollectionElement)) { + return false; + } + IteratedCollectionElement other = (IteratedCollectionElement) obj; + return other.tree.equals(this.tree) + || other.tree == this.tree + || other.node == this.node + || other.node.equals(this.node); + } + + // /** + // * Returns true if the two elements are the same. + // * + // * @param element1 the first element to compare + // * @param element2 the second element to compare + // * @return true if the two elements are the same + // */ + // protected static boolean sameElement(VariableElement element1, VariableElement element2) { + // VarSymbol vs1 = (VarSymbol) element1; + // VarSymbol vs2 = (VarSymbol) element2; + // // If a LocalVariable is created via JavaExpressionParseUtil#parse, then `vs1.equals(vs2)` + // // will not return true even if the elements represent the same local variable. + // // The owner of a lambda parameter is the enclosing method, so a local variable and a lambda + // // parameter might have the same name and the same owner. Use pos to differentiate this + // // case. + // return vs1.pos == vs2.pos && vs1.name == vs2.name && vs1.owner.equals(vs2.owner); + // } + + // /** + // * Returns the element for this variable. + // * + // * @return the element for this variable + // */ + // public VariableElement getElement() { + // return element; + // } + + @Override + public int hashCode() { + return node.hashCode(); + // return Objects.hash(tree.hashCode()); + } + + // @Override + // public String toString() { + // return var.toString(); + // } + + // @Override + // public String toStringDebug() { + // return super.toStringDebug() + " [owner=" + ((VarSymbol) element).owner + "]"; + // } + + @SuppressWarnings("unchecked") // generic cast + @Override + public @Nullable T containedOfClass(Class clazz) { + return getClass() == clazz ? (T) this : null; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof IteratedCollectionElement)) { + return false; + } + IteratedCollectionElement other = (IteratedCollectionElement) je; + return this.equals(other); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other); + } + + @Override + public boolean isAssignableByOtherCode() { + return false; + } + + @Override + public boolean isModifiableByOtherCode() { + return false; + // return !TypesUtils.isImmutableTypeInJdk(((VarSymbol) element).type); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitIteratedCollectionElement(this, p); + } +} From 9a74bf6e8f076add6776e574ed28ef8144c6ca2c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 21 Jun 2025 18:09:26 +0200 Subject: [PATCH 143/374] handle IteratedCollectionElement JavaExpression --- .../calledmethods/CalledMethodsTransfer.java | 2 +- .../dataflow/cfg/visualize/CFGVisualizer.java | 11 ++++ .../cfg/visualize/DOTCFGVisualizer.java | 6 ++ .../cfg/visualize/StringCFGVisualizer.java | 6 ++ .../expression/JavaExpressionConverter.java | 6 ++ .../expression/JavaExpressionScanner.java | 6 ++ .../expression/JavaExpressionVisitor.java | 10 ++++ .../framework/flow/CFAbstractStore.java | 59 +++++++++++++++++++ 8 files changed, 105 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java index 1eb71d3c0544..85aec110e04c 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java @@ -43,7 +43,7 @@ public class CalledMethodsTransfer extends AccumulationTransfer { * The element for the CalledMethods annotation's value element. Stored in a field in this class * to prevent the need to cast to CalledMethods ATF every time it's used. */ - private final ExecutableElement calledMethodsValueElement; + protected final ExecutableElement calledMethodsValueElement; /** The type mirror for {@link Exception}. */ protected final TypeMirror javaLangExceptionType; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java index 116fa08f39bd..bc5269137acc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java @@ -14,6 +14,7 @@ import org.checkerframework.dataflow.expression.ArrayAccess; import org.checkerframework.dataflow.expression.ClassName; import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.MethodCall; @@ -102,6 +103,16 @@ public interface CFGVisualizer< */ String visualizeStoreLocalVar(LocalVariable localVar, V value); + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize an iterated collection + * element. + * + * @param ice the iterated collection element + * @param value the value of the local variable + * @return the String representation of the local variable + */ + String visualizeStoreIteratedCollectionElt(IteratedCollectionElement ice, V value); + /** * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of the current * object {@code this} in this Store. diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java index ff526cb125c8..7c7ad55c7e80 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java @@ -37,6 +37,7 @@ import org.checkerframework.dataflow.expression.ArrayAccess; import org.checkerframework.dataflow.expression.ClassName; import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.MethodCall; import org.checkerframework.javacutil.BugInCF; @@ -297,6 +298,11 @@ public String visualizeStoreLocalVar(LocalVariable localVar, V value) { return storeEntryIndent + localVar + " > " + escapeString(value); } + @Override + public String visualizeStoreIteratedCollectionElt(IteratedCollectionElement ice, V value) { + return storeEntryIndent + ice + " > " + escapeString(value); + } + @Override public String visualizeStoreFieldVal(FieldAccess fieldAccess, V value) { return storeEntryIndent + fieldAccess + " > " + escapeString(value); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java index 4cef0a8e26a2..c9ea4a5d05a0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java @@ -21,6 +21,7 @@ import org.checkerframework.dataflow.expression.ArrayAccess; import org.checkerframework.dataflow.expression.ClassName; import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.MethodCall; @@ -135,6 +136,11 @@ public String visualizeStoreLocalVar(LocalVariable localVar, V value) { return storeEntryIndent + localVar + " > " + value; } + @Override + public String visualizeStoreIteratedCollectionElt(IteratedCollectionElement ice, V value) { + return storeEntryIndent + ice + " > " + value; + } + @Override public String visualizeStoreFieldVal(FieldAccess fieldAccess, V value) { return storeEntryIndent + fieldAccess + " > " + value; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java index 78066c112251..e6a010c0e6fb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java @@ -89,6 +89,12 @@ protected JavaExpression visitLocalVariable(LocalVariable localVarExpr, Void unu return localVarExpr; } + @Override + protected JavaExpression visitIteratedCollectionElement( + IteratedCollectionElement iteratedCollectionElt, Void unused) { + return iteratedCollectionElt; + } + @Override protected JavaExpression visitMethodCall(MethodCall methodCallExpr, Void unused) { JavaExpression receiver = convert(methodCallExpr.getReceiver()); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java index 79c9eb0f158f..1b9fb9abe6cb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java @@ -76,6 +76,12 @@ protected Void visitLocalVariable(LocalVariable localVarExpr, P p) { return null; } + @Override + protected Void visitIteratedCollectionElement( + IteratedCollectionElement iteratedCollectionElement, P p) { + return null; + } + @Override protected Void visitMethodCall(MethodCall methodCallExpr, P p) { visit(methodCallExpr.getReceiver(), p); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java index 2fa169f8bc87..b798371aeb17 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java @@ -82,6 +82,16 @@ public R visit(JavaExpression javaExpr, P p) { */ protected abstract R visitLocalVariable(LocalVariable localVarExpr, P p); + /** + * Visit a {@link IteratedCollectionElement}. + * + * @param iteratedCollectionElt the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code localVarExpr} + */ + protected abstract R visitIteratedCollectionElement( + IteratedCollectionElement iteratedCollectionElt, P p); + /** * Visit a {@link MethodCall}. * diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index 54fb7004d4c1..0d0caddd4ce4 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.flow; +import com.sun.source.tree.Tree; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -30,6 +31,7 @@ import org.checkerframework.dataflow.expression.ArrayAccess; import org.checkerframework.dataflow.expression.ClassName; import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.MethodCall; @@ -72,6 +74,9 @@ public abstract class CFAbstractStore, S extends CF /** Information collected about local variables (including method parameters). */ protected final Map localVariableValues; + /** Information collected about iterated collection elements. */ + protected final Map iteratedCollectionElements; + /** Information collected about the current object. */ protected V thisValue; @@ -142,6 +147,7 @@ public long getUid() { protected CFAbstractStore(CFAbstractAnalysis analysis, boolean sequentialSemantics) { this.analysis = analysis; this.localVariableValues = new HashMap<>(); + this.iteratedCollectionElements = new HashMap<>(); this.thisValue = null; this.fieldValues = new HashMap<>(); this.methodCallExpressions = new HashMap<>(); @@ -162,6 +168,7 @@ protected CFAbstractStore(CFAbstractAnalysis analysis, boolean sequenti protected CFAbstractStore(CFAbstractStore other) { this.analysis = other.analysis; this.localVariableValues = new HashMap<>(other.localVariableValues); + this.iteratedCollectionElements = new HashMap<>(other.iteratedCollectionElements); this.thisValue = other.thisValue; this.fieldValues = new HashMap<>(other.fieldValues); this.methodCallExpressions = new HashMap<>(other.methodCallExpressions); @@ -497,6 +504,7 @@ public static boolean canInsertJavaExpression(JavaExpression expr) { || expr instanceof ThisReference || expr instanceof SuperReference || expr instanceof LocalVariable + || expr instanceof IteratedCollectionElement || expr instanceof MethodCall || expr instanceof ArrayAccess || expr instanceof ClassName) { @@ -631,6 +639,13 @@ protected void computeNewValueAndInsert( if (newValue != null) { localVariableValues.put(localVar, newValue); } + } else if (expr instanceof IteratedCollectionElement) { + IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + V oldValue = iteratedCollectionElements.get(collectionElt); + V newValue = merger.apply(oldValue, value); + if (newValue != null) { + iteratedCollectionElements.put(collectionElt, newValue); + } } else if (expr instanceof FieldAccess) { FieldAccess fieldAcc = (FieldAccess) expr; // Only store information about final fields (where the receiver is @@ -761,6 +776,9 @@ public void clearValue(JavaExpression expr) { if (expr instanceof LocalVariable) { LocalVariable localVar = (LocalVariable) expr; localVariableValues.remove(localVar); + } else if (expr instanceof IteratedCollectionElement) { + IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + iteratedCollectionElements.remove(collectionElt); } else if (expr instanceof FieldAccess) { FieldAccess fieldAcc = (FieldAccess) expr; fieldValues.remove(fieldAcc); @@ -780,6 +798,23 @@ public void clearValue(JavaExpression expr) { } } + /** + * Returns the {@link IteratedCollectionElement} associated with the given node or tree. + * + * @param node CFG node for which the associated {@link IteratedCollectionElement} is sought + * @param tree AST tree for which the associated {@link IteratedCollectionElement} is sought + * @return the {@link IteratedCollectionElement} associated with the given node or tree. + */ + @SuppressWarnings("interning:not.interned") // we want to check reference equality + public @Nullable IteratedCollectionElement getIteratedCollectionElement(Node node, Tree tree) { + for (IteratedCollectionElement ice : iteratedCollectionElements.keySet()) { + if (ice.tree == tree || ice.node == node || ice.tree.equals(tree) || ice.node.equals(node)) { + return ice; + } + } + return null; + } + /** * Returns the current abstract value of a Java expression, or {@code null} if no information is * available. @@ -791,6 +826,9 @@ public void clearValue(JavaExpression expr) { if (expr instanceof LocalVariable) { LocalVariable localVar = (LocalVariable) expr; return localVariableValues.get(localVar); + } else if (expr instanceof IteratedCollectionElement) { + IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + return iteratedCollectionElements.get(collectionElt); } else if (expr instanceof ThisReference || expr instanceof SuperReference) { return thisValue; } else if (expr instanceof FieldAccess) { @@ -1189,6 +1227,16 @@ private S upperBound(S other, boolean shouldWiden) { } } + for (Map.Entry e : other.iteratedCollectionElements.entrySet()) { + IteratedCollectionElement ice = e.getKey(); + V thisVal = iteratedCollectionElements.get(ice); + if (thisVal != null) { + V otherVal = e.getValue(); + V mergedVal = upperBoundOfValues(otherVal, thisVal, shouldWiden); + newStore.iteratedCollectionElements.put(ice, mergedVal); + } + } + // information about the current object { V otherVal = other.thisValue; @@ -1271,6 +1319,13 @@ protected boolean supersetOf(CFAbstractStore other) { return false; } } + for (Map.Entry e : other.iteratedCollectionElements.entrySet()) { + IteratedCollectionElement key = e.getKey(); + V value = iteratedCollectionElements.get(key); + if (value == null || !value.equals(e.getValue())) { + return false; + } + } if (!Objects.equals(thisValue, other.thisValue)) { return false; } @@ -1354,6 +1409,10 @@ protected String internalVisualize(CFGVisualizer viz) { for (LocalVariable lv : ToStringComparator.sorted(localVariableValues.keySet())) { res.add(viz.visualizeStoreLocalVar(lv, localVariableValues.get(lv))); } + for (IteratedCollectionElement ice : + ToStringComparator.sorted(iteratedCollectionElements.keySet())) { + res.add(viz.visualizeStoreIteratedCollectionElt(ice, iteratedCollectionElements.get(ice))); + } if (thisValue != null) { res.add(viz.visualizeStoreThisVal(thisValue)); } From 4111e30bad89dff4cb8e3afaba4d91cc1de9f98a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 00:16:14 +0200 Subject: [PATCH 144/374] remove obligation in else edge of fulfilling loop conditional block --- ...llectionOwnershipAnnotatedTypeFactory.java | 47 +++++++++++++++- .../MustCallConsistencyAnalyzer.java | 56 +++++++++++++++++++ .../RLCCalledMethodsAnnotatedTypeFactory.java | 12 +++- 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ac03fd314785..e490fd08de33 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -2,11 +2,14 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -89,8 +92,50 @@ public enum CollectionOwnershipType { */ public static final String UNKNOWN_METHOD_NAME = "1UNKNOWN"; + /** + * Maps the AST-tree corresponding to the loop condition of a collection-obligation-fulfilling + * loop to the loop wrapper. + */ + private static Map conditionToFulfillingLoopMap = + new HashMap<>(); + + /** + * Maps the cfg-block corresponding to the loop conditional block of a + * collection-obligation-fulfilling loop to the loop wrapper. + */ + private static Map conditionalBlockToFulfillingLoopMap = + new HashMap<>(); + + /** + * Marks the specified loop as fulfilling a collection obligation + * + * @param loop the loop wrapper + */ public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { - // TODO SCK: fill this out + conditionToFulfillingLoopMap.put(loop.condition, loop); + conditionalBlockToFulfillingLoopMap.put(loop.loopConditionalBlock, loop); + } + + /** + * Returns the collection-obligation-fulfilling loop for which the given tree is the condition. + * + * @param tree the loop wrapper + * @return the collection-obligation-fulfilling loop for which the given tree is the condition + */ + public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) { + return conditionToFulfillingLoopMap.get(tree); + } + + /** + * Returns the collection-obligation-fulfilling loop for which the given block is the conditional + * block for. + * + * @param tree the loop wrapper + * @return the collection-obligation-fulfilling loop for which the given block is the conditional + * block for + */ + public static PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Block block) { + return conditionalBlockToFulfillingLoopMap.get(block); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index fdb8ad17d16a..c53428125525 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2210,9 +2210,35 @@ private void propagateObligationsToSuccessorBlock( throw new BugInCF("block with no outgoing incoming store: " + successor); } } + + // check whether this corresponds to the else-cfg-edge of a conditional block + // corresponding to the loop condition of a collection-obligation-fulfilling + // loop. If yes, don't propagate the collection obligations that are fulfilled + // inside the loop. + boolean isElseEdgeOfFulfillingLoop = false; + PotentiallyFulfillingLoop loop = + CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForConditionalBlock(currentBlock); + if ((currentBlock instanceof ConditionalBlock) && loop != null) { + ConditionalBlock conditionalBlock = (ConditionalBlock) currentBlock; + if (conditionalBlock.getElseSuccessor() == successor) { + isElseEdgeOfFulfillingLoop = true; + } + } + // Computed outside the Obligation loop for efficiency. AccumulationStore regularStoreOfSuccessor = input.getRegularStore(); + for (Obligation obligation : obligations) { + if (isElseEdgeOfFulfillingLoop) { + if (obligation instanceof CollectionObligation) { + String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; + if (loop.getMethods().contains(mustCallMethodOfCo)) { + // don't propagate this obligation along this edge, as it was fulfilled + // in the loop that the currentBlock is the conditional block of + continue; + } + } + } // This boolean is true if there is no evidence that the Obligation does not go out // of scope - that is, if there is definitely a resource alias that is in scope in // the successor. @@ -3009,6 +3035,20 @@ private void patternMatchEnhancedCollectionForLoop( } } while (!isLoopCondition); + Block blockContainingLoopCondition = node.getBlock(); + if (blockContainingLoopCondition.getSuccessors().size() != 1) { + throw new BugInCF( + "loop condition has: " + + blockContainingLoopCondition.getSuccessors().size() + + " successors instead of 1."); + } + Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + if (!(conditionalBlock instanceof ConditionalBlock)) { + throw new BugInCF( + "loop condition successor is not ConditionalBlock, but: " + + conditionalBlock.getClass()); + } + // add the blocks into a static datastructure in the calledmethodsatf, such that it can // analyze // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees @@ -3020,6 +3060,7 @@ private void patternMatchEnhancedCollectionForLoop( node.getTree(), loopBodyEntryBlock, block, + (ConditionalBlock) conditionalBlock, loopVarNode); this.analyzeObligationFulfillingLoop(cfg, pfLoop); } @@ -3087,6 +3128,20 @@ private void patternMatchEnhancedArrayForLoop( } } while (!(node instanceof LessThanNode)); + Block blockContainingLoopCondition = node.getBlock(); + if (blockContainingLoopCondition.getSuccessors().size() != 1) { + throw new BugInCF( + "loop condition has: " + + blockContainingLoopCondition.getSuccessors().size() + + " successors instead of 1."); + } + Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + if (!(conditionalBlock instanceof ConditionalBlock)) { + throw new BugInCF( + "loop condition successor is not ConditionalBlock, but: " + + conditionalBlock.getClass()); + } + // add the blocks into a static datastructure in the calledmethodsatf, such that it can // analyze // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees @@ -3098,6 +3153,7 @@ private void patternMatchEnhancedArrayForLoop( node.getTree(), loopBodyEntryBlock, block, + (ConditionalBlock) conditionalBlock, loopVarNode); this.analyzeObligationFulfillingLoop(cfg, pfLoop); } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index a263ebd99b78..f4369d2e2ec3 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -41,6 +41,7 @@ import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -679,12 +680,16 @@ public PotentiallyAssigningLoop( /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ public static class PotentiallyFulfillingLoop extends CollectionObligationAlteringLoop { - /** cfg {@code Block} for the loop body entry */ + + /** cfg {@code Block} containing the loop body entry */ public final Block loopBodyEntryBlock; - /** cfg {@code Block} for the loop update */ + /** cfg {@code Block} containing the loop update */ public final Block loopUpdateBlock; + /** cfg conditional {@link Block} following loop condition */ + public final ConditionalBlock loopConditionalBlock; + /** cfg {@code Node} for the collection element iterated over */ public final Node collectionElementNode; @@ -696,6 +701,7 @@ public static class PotentiallyFulfillingLoop extends CollectionObligationAlteri * @param condition AST {@link Tree} for loop condition * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry * @param loopUpdateBlock cfg {@link Block} for the loop update + * @param loopConditionalBlock cfg conditional {@link Block} following loop condition * @param collectionEltNode cfg {@link Node} for the collection element iterated over */ public PotentiallyFulfillingLoop( @@ -704,6 +710,7 @@ public PotentiallyFulfillingLoop( Tree condition, Block loopBodyEntryBlock, Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, Node collectionEltNode) { super( collectionTree, @@ -713,6 +720,7 @@ public PotentiallyFulfillingLoop( CollectionObligationAlteringLoop.LoopKind.FULFILLING); this.loopBodyEntryBlock = loopBodyEntryBlock; this.loopUpdateBlock = loopUpdateBlock; + this.loopConditionalBlock = loopConditionalBlock; this.collectionElementNode = collectionEltNode; } } From 4337abfcf1733b7a6822553d20389a941b3e2868 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 00:18:54 +0200 Subject: [PATCH 145/374] fix doc --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index e490fd08de33..7a4260792d0c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -119,7 +119,7 @@ public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { /** * Returns the collection-obligation-fulfilling loop for which the given tree is the condition. * - * @param tree the loop wrapper + * @param tree the tree that is potentially the condition for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given tree is the condition */ public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) { @@ -130,7 +130,7 @@ public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) * Returns the collection-obligation-fulfilling loop for which the given block is the conditional * block for. * - * @param tree the loop wrapper + * @param block the block that is potentially the conditional block for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given block is the conditional * block for */ From 8a253489e3447116bc3fa2f676f8fc9681cfa826 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 00:20:54 +0200 Subject: [PATCH 146/374] adapt test outcomes after implementing fulfillment --- .../CollectionOwnershipBasicTyping.java | 2 -- .../resourceleak-collections/CollectionOwnershipDefaults.java | 4 ---- 2 files changed, 6 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 194122c8ffeb..25298a883c1a 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -89,8 +89,6 @@ void testDiamond() { closeElements(col); } - // TODO remove once fulfillment works - // :: error: unfulfilled.collection.obligations void closeElements(@OwningCollection Collection socketCollection) { for (Socket s : socketCollection) { try { diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index 04a87442aed2..ead9c3d2d904 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -125,8 +125,6 @@ void checkNewResourceCollectionDefault() { checkArgIsOCwoO(newResourceArray); } - // TODO remove once fulfillment works - // :: error: unfulfilled.collection.obligations void closeElements(Socket @OwningCollection [] socketCollection) { for (Socket s : socketCollection) { try { @@ -136,8 +134,6 @@ void closeElements(Socket @OwningCollection [] socketCollection) { } } - // TODO remove once fulfillment works - // :: error: unfulfilled.collection.obligations void closeElements(@OwningCollection Collection socketCollection) { for (Socket s : socketCollection) { try { From f5bac6c0c70bad0fa364999623b223529053a901 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 11:39:48 +0200 Subject: [PATCH 147/374] formatting --- .../src/main/java/org/checkerframework/javacutil/TreeUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index e37c614a60a3..415ee9bebefa 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -166,7 +166,7 @@ private TreeUtils() { public static boolean isConstructor(MethodTree tree) { return tree.getName().contentEquals(""); } - + /** * Checks if the method invocation is a call to hasNext. * From 161b0d9d71426e7aa0133e41e93000f7a28f5327 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 12:40:12 +0200 Subject: [PATCH 148/374] make interning checker happy --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index c53428125525..35a3efeee21b 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2220,7 +2220,7 @@ private void propagateObligationsToSuccessorBlock( CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForConditionalBlock(currentBlock); if ((currentBlock instanceof ConditionalBlock) && loop != null) { ConditionalBlock conditionalBlock = (ConditionalBlock) currentBlock; - if (conditionalBlock.getElseSuccessor() == successor) { + if (conditionalBlock.getElseSuccessor().equals(successor)) { isElseEdgeOfFulfillingLoop = true; } } @@ -3193,7 +3193,7 @@ public void analyzeObligationFulfillingLoop( Block loopUpdateBlock = potentiallyFulfillingLoop.loopUpdateBlock; Tree collectionElement = potentiallyFulfillingLoop.collectionElementTree; - boolean emptyLoopBody = loopBodyEntryBlock == loopUpdateBlock; + boolean emptyLoopBody = loopBodyEntryBlock.equals(loopUpdateBlock); if (emptyLoopBody) { return; } From 171c64ab92788e4c44493bfbeff7e77d7530bc1d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 12:43:33 +0200 Subject: [PATCH 149/374] add throws declaration --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 35a3efeee21b..0d712acc950d 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2158,6 +2158,8 @@ private void propagateObligationsToSuccessorBlocks( * control flow * @param visited block-Obligations pairs already analyzed or already on the worklist * @param worklist current worklist + * @throws InvalidLoopBodyAnalysisException if the propagation is called as part of a loop body + * analysis and a state without input (unreachable in conventional analysis) is reached */ private void propagateObligationsToSuccessorBlock( boolean isLoopBodyAnalysis, From 66361fd2769d91ea67d3faf7b5f94a2a7a2d4e4e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 13:28:42 +0200 Subject: [PATCH 150/374] remove println as it makes wpi fail --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 0d712acc950d..64214a737f21 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3020,7 +3020,6 @@ private void patternMatchEnhancedCollectionForLoop( block = predBlocks.iterator().next(); nodeIterator = block.getNodes().iterator(); } else { - System.out.println("predecessor: " + predBlocks); throw new BugInCF( "Encountered more than one CFG Block predeccessor trying to find the" + " enhanced-for-loop update block. Block: "); @@ -3272,7 +3271,6 @@ public void analyzeObligationFulfillingLoop( } } - System.out.println("calledmethodsinloop: " + calledMethodsInLoop); // now put the loop into the static datastructure if it calls any methods on the element if (calledMethodsInLoop != null && calledMethodsInLoop.size() > 0) { potentiallyFulfillingLoop.addMethods(calledMethodsInLoop); @@ -3308,7 +3306,8 @@ private Set analyzeTypeOfCollectionElement( if (collectionElementObligation == null) { // the loop did something weird. Might have reassigned the collection element. // The sound thing to do is return an empty list. - System.out.println("obligation gone for collection element"); + // TODO SCK look at this. Why is the collection element obligaiton gone? + // System.out.println("obligation gone for collection element"); return new HashSet<>(); // throw new BugInCF( // "No obligation for collection element " From dc0e8066aabd0dc9d249621fb1b2e703dc8091b0 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 13:29:05 +0200 Subject: [PATCH 151/374] CollectionOwnership transfer func for fulfilling loop --- .../CollectionOwnershipTransfer.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 605247e08abd..ccfa06931609 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -9,6 +9,8 @@ import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; +import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -107,6 +109,38 @@ public TransferResult visitMethodInvocation( List args = node.getArguments(); res = transferOwnershipForMethodInvocation(method, args, res); + // check if the MethodInvocationNode is the condition for a collection-obligation-fulfilling + // loop, + // and whether the loop calls all methods in the MustCall type of the collection elements. + // In this case, refine the type of the collection to @OwningCollectionWithoutObligation in the + // else + // store. + PotentiallyFulfillingLoop loop = + CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(node.getTree()); + if (loop != null) { + CFStore elseStore = res.getElseStore(); + JavaExpression collectionJx = JavaExpression.fromTree(loop.collectionTree); + CFValue collectionValue = null; + try { + collectionValue = elseStore.getValue(collectionJx); + } catch (Exception e) { + return res; + } + + CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionValue); + if (collectionCoType == CollectionOwnershipType.OwningCollection) { + List mustCallValuesOfElements = + atypeFactory.getMustCallValuesOfResourceCollectionComponent( + collectionValue.getUnderlyingType()); + if (loop.getMethods().containsAll(mustCallValuesOfElements)) { + elseStore.clearValue(collectionJx); + elseStore.insertValue(collectionJx, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); + return new ConditionalTransferResult<>( + res.getResultValue(), res.getThenStore(), elseStore); + } + } + } + // if (!noCreatesMustCallFor) { // List targetExprs = // CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( From 026c94b0be69651636bb68fed15e591531807f7f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 15:14:43 +0200 Subject: [PATCH 152/374] override .equals(Object) for CollectionObligation --- .../resourceleak/MustCallConsistencyAnalyzer.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 64214a737f21..8142fcfe1601 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -560,6 +560,16 @@ public CollectionObligation getReplacement( Set resourceAliases, Set whenToEnforce) { return new CollectionObligation(this.mustCallMethod, resourceAliases, whenToEnforce); } + + @Override + public boolean equals(@Nullable Object obj) { + if (!super.equals(obj)) { + return false; + } else { + return (obj instanceof CollectionObligation) + && ((CollectionObligation) obj).mustCallMethod == this.mustCallMethod; + } + } } // Is there a different Obligation on every line of the program, or is Obligation mutable? From c732cee793d0816b5d1cca614cc57ba02360a066 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 15:29:56 +0200 Subject: [PATCH 153/374] tests for loop body analysis --- .../LoopBodyAnalysisTest.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java new file mode 100644 index 000000000000..aab7354f9a53 --- /dev/null +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -0,0 +1,123 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall({"flush"}) +class Resource implements AutoCloseable { + @Override + public void close() {} + + void flush() {} +} + +class LoopBodyAnalysisTests { + + void fullSatisfyArray(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + r.close(); + r.flush(); + } + } + + void fullSatisfyCollection(@OwningCollection Collection resources) { + for (Resource r : resources) { + r.close(); + r.flush(); + } + } + + // :: error: unfulfilled.collection.obligations + void partialSatisfyArrayShouldError(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + r.close(); + } + } + + // :: error: unfulfilled.collection.obligations + void partialSatisfyCollectionShouldError(@OwningCollection Collection resources) { + for (Resource r : resources) { + r.close(); + } + } + + void multipleMustCallPartial() { + // :: error: unfulfilled.collection.obligations + List l = new ArrayList<>(); + l.add(new Resource()); + l.add(new Resource()); + for (Resource r : l) { + r.close(); + } + } + + void multipleMustCallFull() { + List l = new ArrayList<>(); + l.add(new Resource()); + l.add(new Resource()); + for (Resource r : l) { + r.close(); + r.flush(); + } + } + + void tryCatchShouldWork(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + try { + r.close(); + r.flush(); + } catch (Exception e) { + } + } + } + + void methodCallInsideLoop(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + doCloseFlush(r); + } + } + + // :: error: unfulfilled.collection.obligations + void earlyBreak(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + r.close(); + r.flush(); + break; // not all elements visited + } + } + + void tryWithResources(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + try (Resource auto = r) { + auto.flush(); + } + } + } + + void nullableElementWithCheck(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + if (r != null) { + r.close(); + r.flush(); + } + } + } + + void nullableElementHelper(Resource @OwningCollection [] resources) { + for (Resource r : resources) { + if (r != null) { + doCloseFlush(r); + } + } + } + + @EnsuresCalledMethods( + value = "#1", + methods = {"close", "flush"}) + void doCloseFlush(Resource r) { + r.close(); + r.flush(); + } +} From 81dd857ddf37d455bc4af6cd56c2779f450bea9e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:00:24 +0200 Subject: [PATCH 154/374] mark desugared assignment --- .../cfg/builder/CFGTranslationPhaseOne.java | 4 +++- .../dataflow/cfg/node/AssignmentNode.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index ca3aae596c5f..aaf0de3d39cf 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -3087,7 +3087,9 @@ public Node visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { extendWithNode(arrayVariableNode); Node expressionNode = scan(expression, p); - translateAssignment(arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); + AssignmentNode assignment = + translateAssignment(arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); + assignment.setDesugared(); // Declare and initialize the loop index variable TypeMirror intType = types.getPrimitiveType(TypeKind.INT); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java index 8ea0dc959a54..ca5e787f36a9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java @@ -47,6 +47,9 @@ public class AssignmentNode extends Node { /** Whether the assignment node is synthetic */ protected final boolean synthetic; + /** Whether the assignment node is desugared from an enhanced-for-loop over an array. */ + protected boolean desugared; + /** * Create a (non-synthetic) AssignmentNode. * @@ -79,6 +82,7 @@ public AssignmentNode(Tree tree, Node target, Node expression, boolean synthetic this.lhs = target; this.rhs = expression; this.synthetic = synthetic; + this.desugared = false; } /** @@ -117,6 +121,20 @@ public boolean isSynthetic() { return synthetic; } + /** + * Check if the assignment node is desugared from an enhanced-for-loop over an array. + * + * @return true if the assignment node is desugared + */ + public boolean isDesugared() { + return desugared; + } + + /** set the assignment node as desugared from an enhanced-for-loop over an array */ + public void setDesugared() { + desugared = true; + } + @Override public R accept(NodeVisitor visitor, P p) { return visitor.visitAssignment(this, p); From fea139b9183d908d8e89a99bdd0a90e212ccf90a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:00:59 +0200 Subject: [PATCH 155/374] special case desugared array assignment --- .../CollectionOwnershipTransfer.java | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index ccfa06931609..bc2fc3173d6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership; +import com.sun.source.tree.Tree; import java.util.HashSet; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -15,6 +16,7 @@ import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.LessThanNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -58,6 +60,11 @@ public TransferResult visitAssignment( TransferResult res = super.visitAssignment(node, in); CFStore store = res.getRegularStore(); + + Node lhs = node.getTarget(); + lhs = getNodeOrTempVar(lhs); + JavaExpression lhsJx = JavaExpression.fromNode(lhs); + Node rhs = node.getExpression(); rhs = getNodeOrTempVar(rhs); JavaExpression rhsJx = JavaExpression.fromNode(rhs); @@ -70,11 +77,17 @@ public TransferResult visitAssignment( } CollectionOwnershipType rhsType = atypeFactory.getCoType(rhsValue); - // ownership transfer from rhs into lhs + // ownership transfer from rhs into lhs usually. + // special case desugared assignments of a temporary array variable. if (rhsType == CollectionOwnershipType.OwningCollection || rhsType == CollectionOwnershipType.OwningCollectionWithoutObligation) { - store.clearValue(rhsJx); - store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + if (node.isDesugared()) { + store.clearValue(lhsJx); + store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } else { + store.clearValue(rhsJx); + store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } } // boolean assignmentOfOwningCollectionArrayElement = @@ -98,25 +111,21 @@ public TransferResult visitAssignment( return new RegularTransferResult(res.getResultValue(), store); } - @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode node, TransferInput in) { - TransferResult res = super.visitMethodInvocation(node, in); - - updateStoreWithTempVar(res, node); - - ExecutableElement method = node.getTarget().getMethod(); - List args = node.getArguments(); - res = transferOwnershipForMethodInvocation(method, args, res); - - // check if the MethodInvocationNode is the condition for a collection-obligation-fulfilling - // loop, - // and whether the loop calls all methods in the MustCall type of the collection elements. - // In this case, refine the type of the collection to @OwningCollectionWithoutObligation in the - // else - // store. + /** + * Checks if the given AST tree is the condition for a collection-obligation-fulfilling and + * whether this loop calls all methods in the MustCall type of the elements of some collection. In + * this case, refine the type of the collection to @OwningCollectionWithoutObligation in the else + * store of the incoming transfer result. + * + * @param res the incoming transfer result + * @param tree the AST tree that is possibly the loop condition for a + * collection-obligation-fulfilling loop + * @return the resulting transfer result + */ + private TransferResult transformPotentiallyFulfillingLoop( + TransferResult res, Tree tree) { PotentiallyFulfillingLoop loop = - CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(node.getTree()); + CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); if (loop != null) { CFStore elseStore = res.getElseStore(); JavaExpression collectionJx = JavaExpression.fromTree(loop.collectionTree); @@ -140,6 +149,28 @@ public TransferResult visitMethodInvocation( } } } + return res; + } + + @Override + public TransferResult visitLessThan( + LessThanNode node, TransferInput in) { + TransferResult res = super.visitLessThan(node, in); + return transformPotentiallyFulfillingLoop(res, node.getTree()); + } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode node, TransferInput in) { + TransferResult res = super.visitMethodInvocation(node, in); + + updateStoreWithTempVar(res, node); + + ExecutableElement method = node.getTarget().getMethod(); + List args = node.getArguments(); + res = transferOwnershipForMethodInvocation(method, args, res); + + res = transformPotentiallyFulfillingLoop(res, node.getTree()); // if (!noCreatesMustCallFor) { // List targetExprs = From e5e29dafc08311b3df4c68badd8181e704dd66e5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:01:23 +0200 Subject: [PATCH 156/374] make interning checker happy one more time --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 8142fcfe1601..cad9672b5e8f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -567,7 +567,7 @@ public boolean equals(@Nullable Object obj) { return false; } else { return (obj instanceof CollectionObligation) - && ((CollectionObligation) obj).mustCallMethod == this.mustCallMethod; + && ((CollectionObligation) obj).mustCallMethod.equals(this.mustCallMethod); } } } From 8e49c89472d82ec058555e204287127ea86ec5b7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:01:42 +0200 Subject: [PATCH 157/374] more loop body analysis tests --- .../LoopBodyAnalysisTest.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index aab7354f9a53..6ede38f91cd5 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -15,18 +15,42 @@ void flush() {} class LoopBodyAnalysisTests { + void fullSatisfyCollection(@OwningCollection Collection resources) { + for (Resource r : resources) { + r.close(); + r.flush(); + } + checkArgIsOCWO(resources); + } + void fullSatisfyArray(Resource @OwningCollection [] resources) { for (Resource r : resources) { r.close(); r.flush(); } + checkArgIsOCWO(resources); } - void fullSatisfyCollection(@OwningCollection Collection resources) { + // here, the argument defaults to @NotOwningCollection. + // the loop should not change that type + void fullSatisfyCollectionNotOwning(Collection resources) { + for (Resource r : resources) { + r.close(); + r.flush(); + } + // :: error: argument + checkArgIsOCWO(resources); + } + + // here, the argument defaults to @NotOwningCollection. + // the loop should not change that type + void fullSatisfyArrayNotOwning(Resource[] resources) { for (Resource r : resources) { r.close(); r.flush(); } + // :: error: argument + checkArgIsOCWO(resources); } // :: error: unfulfilled.collection.obligations @@ -34,6 +58,8 @@ void partialSatisfyArrayShouldError(Resource @OwningCollection [] resources) { for (Resource r : resources) { r.close(); } + // :: error: argument + checkArgIsOCWO(resources); } // :: error: unfulfilled.collection.obligations @@ -41,26 +67,29 @@ void partialSatisfyCollectionShouldError(@OwningCollection Collection for (Resource r : resources) { r.close(); } + // :: error: argument + checkArgIsOCWO(resources); } void multipleMustCallPartial() { // :: error: unfulfilled.collection.obligations List l = new ArrayList<>(); l.add(new Resource()); - l.add(new Resource()); for (Resource r : l) { r.close(); } + // :: error: argument + checkArgIsOCWO(l); } void multipleMustCallFull() { List l = new ArrayList<>(); l.add(new Resource()); - l.add(new Resource()); for (Resource r : l) { r.close(); r.flush(); } + checkArgIsOCWO(l); } void tryCatchShouldWork(Resource @OwningCollection [] resources) { @@ -120,4 +149,8 @@ void doCloseFlush(Resource r) { r.close(); r.flush(); } + + void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} + + void checkArgIsOCWO(Resource @OwningCollectionWithoutObligation [] arg) {} } From 7419cfa9b40f38674758f92a91be39a9b3ea496d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:12:15 +0200 Subject: [PATCH 158/374] expand doc on assignment node marking of desugaring --- .../dataflow/cfg/node/AssignmentNode.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java index ca5e787f36a9..456c62a35528 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java @@ -31,6 +31,9 @@ *

String concatenation compound assignments are desugared to an assignment and a string * concatenation. * + *

Assignments desugared from an enhanced-for-loop over an array are marked as such for special + * casing. + * *

Numeric compound assignments are desugared to an assignment and a numeric operation. */ public class AssignmentNode extends Node { @@ -48,7 +51,7 @@ public class AssignmentNode extends Node { protected final boolean synthetic; /** Whether the assignment node is desugared from an enhanced-for-loop over an array. */ - protected boolean desugared; + protected boolean desugaredFromEnhancedArrayForLoop; /** * Create a (non-synthetic) AssignmentNode. @@ -82,7 +85,7 @@ public AssignmentNode(Tree tree, Node target, Node expression, boolean synthetic this.lhs = target; this.rhs = expression; this.synthetic = synthetic; - this.desugared = false; + this.desugaredFromEnhancedArrayForLoop = false; } /** @@ -126,13 +129,13 @@ public boolean isSynthetic() { * * @return true if the assignment node is desugared */ - public boolean isDesugared() { - return desugared; + public boolean isDesugaredFromEnhancedArrayForLoop() { + return desugaredFromEnhancedArrayForLoop; } /** set the assignment node as desugared from an enhanced-for-loop over an array */ - public void setDesugared() { - desugared = true; + public void setDesugaredFromEnhancedArrayForLoop() { + desugaredFromEnhancedArrayForLoop = true; } @Override From 8fbd9ffde4db10da3a551c69548d16a505d19760 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:13:15 +0200 Subject: [PATCH 159/374] fix method call to desugaring marking in cfg translation --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index aaf0de3d39cf..15bd04cc32c2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -3089,7 +3089,7 @@ public Node visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { AssignmentNode assignment = translateAssignment(arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); - assignment.setDesugared(); + assignment.setDesugaredFromEnhancedArrayForLoop(); // Declare and initialize the loop index variable TypeMirror intType = types.getPrimitiveType(TypeKind.INT); From 3819423f684c65d5cf036b532e338bea04b0bb50 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 17:26:13 +0200 Subject: [PATCH 160/374] formatting --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 15bd04cc32c2..5b579789635c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -3087,9 +3087,8 @@ public Node visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { extendWithNode(arrayVariableNode); Node expressionNode = scan(expression, p); - AssignmentNode assignment = - translateAssignment(arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); - assignment.setDesugaredFromEnhancedArrayForLoop(); + translateAssignment(arrayVariable, new LocalVariableNode(arrayVariable), expressionNode) + .setDesugaredFromEnhancedArrayForLoop(); // Declare and initialize the loop index variable TypeMirror intType = types.getPrimitiveType(TypeKind.INT); From 794a2bfec7361f756e730969778af5f3359d1f1c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 18:38:00 +0200 Subject: [PATCH 161/374] fix call to isdesugared() --- .../collectionownership/CollectionOwnershipTransfer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index bc2fc3173d6a..3ae54ca15012 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -81,7 +81,7 @@ public TransferResult visitAssignment( // special case desugared assignments of a temporary array variable. if (rhsType == CollectionOwnershipType.OwningCollection || rhsType == CollectionOwnershipType.OwningCollectionWithoutObligation) { - if (node.isDesugared()) { + if (node.isDesugaredFromEnhancedArrayForLoop()) { store.clearValue(lhsJx); store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); } else { From 95dc4f87f9da415be101a941e520945232659b0c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 18:52:27 +0200 Subject: [PATCH 162/374] add infrastructure for loop-body-analysis on normal for-loops --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 67 +++++++++++++++ .../RLCCalledMethodsTransfer.java | 84 +++++++++++++++++++ .../accumulation/AccumulationTransfer.java | 2 +- 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index f4369d2e2ec3..3ecb2ac8dd67 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -109,6 +109,52 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated */ private final BiMap tempVarToTree = HashBiMap.create(); + /** + * Set of potentially collection-obligation-fulfilling loops. Found in the MustCallVisitor, which + * checks the header and (parts of the) body. + */ + private static final Set potentiallyFulfillingLoops = new HashSet<>(); + + /** + * Construct a {@code PotentiallyFulfillingLoop} and add it to the static set of such loops to + * have their loop body analyzed for possibly fulfilling collection obligations. + * + * @param collectionTree AST {@code Tree} for collection iterated over + * @param collectionElementTree AST {@code Tree} for collection element iterated over + * @param condition AST {@code Tree} for loop condition + * @param loopBodyEntryBlock cfg {@code Block} for loop body entry + * @param loopUpdateBlock {@code Block} containing loop update + * @param loopConditionalBlock {@code Block} containing loop condition + * @param collectionEltNode cfg {@code Node} for collection element iterated over + */ + public static void addPotentiallyFulfillingLoop( + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree condition, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, + Node collectionEltNode) { + potentiallyFulfillingLoops.add( + new PotentiallyFulfillingLoop( + collectionTree, + collectionElementTree, + condition, + loopBodyEntryBlock, + loopUpdateBlock, + loopConditionalBlock, + collectionEltNode)); + } + + /** + * Return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. + * + * @return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. + */ + public static Set getPotentiallyFulfillingLoops() { + return potentiallyFulfillingLoops; + } + /** * Creates a new RLCCalledMethodsAnnotatedTypeFactory. * @@ -739,10 +785,31 @@ public PotentiallyFulfillingLoop( public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this)); + + // traverse the cfg to find enhanced-for-loops over collections and perform a + // loop-body-analysis. // since this runs before the consistency analysis, we unfortunately have to traverse the cfg // twice. The first time to find these for-each loop, and the second time much later to do the // final consistency analysis. mustCallConsistencyAnalyzer.findFulfillingForEachLoops(cfg); + + // perform loop-body-analysis on normal for-loops that were pattern matched on the AST + if (potentiallyFulfillingLoops.size() > 0) { + Set analyzed = new HashSet<>(); + for (PotentiallyFulfillingLoop potentiallyFulfillingLoop : potentiallyFulfillingLoops) { + Tree collectionElementTree = potentiallyFulfillingLoop.collectionElementTree; + boolean loopContainedInThisMethod = + cfg.getNodesCorrespondingToTree(collectionElementTree) != null; + if (loopContainedInThisMethod) { + System.out.println("analyzing loop " + potentiallyFulfillingLoop.collectionTree); + mustCallConsistencyAnalyzer.analyzeObligationFulfillingLoop( + cfg, potentiallyFulfillingLoop); + analyzed.add(potentiallyFulfillingLoop); + } + } + potentiallyFulfillingLoops.removeAll(analyzed); + } + super.postAnalyze(cfg); } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 28bd017fafec..9f83782a2c37 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -1,10 +1,13 @@ package org.checkerframework.checker.rlccalledmethods; import com.sun.source.tree.MethodInvocationTree; +import java.util.Arrays; import java.util.List; +import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.CalledMethodsTransfer; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; @@ -14,13 +17,17 @@ import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; +import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -45,6 +52,83 @@ public RLCCalledMethodsTransfer(RLCCalledMethodsAnalysis analysis) { this.rlTypeFactory = (RLCCalledMethodsAnnotatedTypeFactory) analysis.getTypeFactory(); } + @Override + public void accumulate( + Node node, TransferResult result, String... values) { + super.accumulate(node, result, values); + updateStoreForIteratedCollectionElement(Arrays.asList(values), result, node); + } + + /** + * Add the collection elements iterated over in potentially Mcoe-obligation-fulfilling loops to + * the store. + */ + @Override + public AccumulationStore initialStore( + UnderlyingAST underlyingAST, List parameters) { + AccumulationStore store = super.initialStore(underlyingAST, parameters); + RLCCalledMethodsAnnotatedTypeFactory cmAtf = + (RLCCalledMethodsAnnotatedTypeFactory) this.analysis.getTypeFactory(); + for (RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop loop : + RLCCalledMethodsAnnotatedTypeFactory.getPotentiallyFulfillingLoops()) { + IteratedCollectionElement collectionElementJx = + new IteratedCollectionElement(loop.collectionElementNode, loop.collectionElementTree); + store.insertValue(collectionElementJx, cmAtf.top); + } + return store; + } + + /** + * Checks whether the given node is the element of a collection iterated over in a + * potentially-Mcoe-obligation-fulfilling loop. If it is, accumulates the called methods to this + * collection element. + * + * @param valuesAsList the list of called methods + * @param result the transfer result + * @param node a cfg node + */ + private void updateStoreForIteratedCollectionElement( + List valuesAsList, + TransferResult result, + Node node) { + IteratedCollectionElement collectionElement = + result.getRegularStore().getIteratedCollectionElement(node, node.getTree()); + if (collectionElement != null) { + AccumulationValue flowValue = result.getRegularStore().getValue(collectionElement); + if (flowValue != null) { + // Dataflow has already recorded information about the target. Integrate it into + // the list of values in the new annotation. + AnnotationMirrorSet flowAnnos = flowValue.getAnnotations(); + assert flowAnnos.size() <= 1; + for (AnnotationMirror anno : flowAnnos) { + if (atypeFactory.isAccumulatorAnnotation(anno)) { + List oldFlowValues = + AnnotationUtils.getElementValueArray(anno, calledMethodsValueElement, String.class); + // valuesAsList cannot have its length changed -- it is backed by an + // array. getElementValueArray returns a new, modifiable list. + oldFlowValues.addAll(valuesAsList); + valuesAsList = oldFlowValues; + } + } + } + AnnotationMirror newAnno = atypeFactory.createAccumulatorAnnotation(valuesAsList); + if (result.containsTwoStores()) { + updateValueAndInsertIntoStore(result.getThenStore(), collectionElement, valuesAsList); + updateValueAndInsertIntoStore(result.getElseStore(), collectionElement, valuesAsList); + } else { + updateValueAndInsertIntoStore(result.getRegularStore(), collectionElement, valuesAsList); + } + Map exceptionalStores = result.getExceptionalStores(); + exceptionalStores.forEach( + (tm, s) -> + s.replaceValue( + collectionElement, + analysis.createSingleAnnotationValue(newAnno, collectionElement.getType()))); + // TODO unsure this is necessary/harmful + result.withExceptionalStores(exceptionalStores); + } + } + @Override public TransferResult visitTernaryExpression( TernaryExpressionNode node, TransferInput input) { diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java index 00967e564f0f..d5b87f9f104f 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java @@ -102,7 +102,7 @@ public void accumulate( * returned true) * @param values a list of newly-accumulated values */ - private void updateValueAndInsertIntoStore( + protected void updateValueAndInsertIntoStore( AccumulationStore store, JavaExpression target, List values) { // Make a modifiable copy of the list. List valuesAsList = new ArrayList<>(values); From 1c3ad701757c6ed8ef44fd7f3f60d239cf193035 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 19:31:30 +0200 Subject: [PATCH 163/374] add treeutils helper methods --- .../checkerframework/javacutil/TreeUtils.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 415ee9bebefa..3fca170fa935 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1717,6 +1717,35 @@ && getFieldName(tree).equals("length")) { return false; } + /** + * If the given tree is a call to "get", this method returns the expression tree of the first + * argument and null else. + * + *

Assuming it's a call to List.get(), this will be the iterator variable. For example, if the + * tree is {@code Collection.get(i)}, the method returns {@code i}. + * + * @param tree the tree to check + * @return ExpressionTree of {@code idx} if tree is {@code Collection.get(idx)} and null else + */ + public static @Nullable ExpressionTree getIdxForGetCall(Tree tree) { + if (tree.getKind() == Tree.Kind.METHOD_INVOCATION + && isNamedMethodCall("get", (MethodInvocationTree) tree)) { + return ((MethodInvocationTree) tree).getArguments().get(0); + } + return null; + } + + /** + * Returns whether the given tree is of the form object.size() + * + * @param tree the tree to check + * @return whether the given tree is of the form object.size() + */ + public static boolean isSizeAccess(Tree tree) { + return tree.getKind() == Tree.Kind.METHOD_INVOCATION + && isNamedMethodCall("size", (MethodInvocationTree) tree); + } + /** * Returns true if the given {@link MethodTree} is an anonymous constructor (the constructor for * an anonymous class). From ecf6bc6af8222d807bcbd6524e1ab0f2e0f59dad Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 22:12:15 +0200 Subject: [PATCH 164/374] adapt consistency analyzer for loop body analysis over normal for-loops with ICE --- ...llectionOwnershipAnnotatedTypeFactory.java | 3 +- .../MustCallConsistencyAnalyzer.java | 96 ++++++++++++++++--- .../RLCCalledMethodsAnnotatedTypeFactory.java | 3 +- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 7a4260792d0c..9a21fea3a0f1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -192,7 +192,8 @@ public void postAnalyze(ControlFlowGraph cfg) { (RLCCalledMethodsAnnotatedTypeFactory) ResourceLeakUtils.getRLCCalledMethodsChecker(this).getTypeFactory(); rlc.setRoot(root); - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = + new MustCallConsistencyAnalyzer(rlc, false); mustCallConsistencyAnalyzer.analyze(cfg); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for // finalizer methods and @InheritableMustCall annotations for the class declarations. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index cad9672b5e8f..da9c20c64893 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -176,6 +176,12 @@ public class MustCallConsistencyAnalyzer { /** The type factory for the Collection Ownership Checker. */ private final CollectionOwnershipAnnotatedTypeFactory coAtf; + /** + * True if currently executing a loop body analysis and false if executing a normal consistency + * analysis. + */ + private boolean isLoopBodyAnalysis; + /** * A cache for the result of calling {@code RLCCalledMethodsAnnotatedTypeFactory.getStoreAfter()} * on a node. The cache prevents repeatedly computing least upper bounds on stores @@ -701,11 +707,12 @@ public String stringForErrorMessage() { * * @param rlc the resource leak checker */ - public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc) { + public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAnalysis) { this.cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) ResourceLeakUtils.getRLCCalledMethodsChecker(rlc).getTypeFactory(); this.coAtf = ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(cmAtf); + this.isLoopBodyAnalysis = isLoopBodyAnalysis; this.checker = rlc; this.permitStaticOwning = checker.hasOption("permitStaticOwning"); this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); @@ -1317,8 +1324,8 @@ private void updateObligationsForAssignment( return; } // Use the temporary variable for the rhs if it exists. - Node rhs = NodeUtils.removeCasts(assignmentNode.getExpression()); - rhs = getTempVarOrNode(rhs); + Node rhsExpr = NodeUtils.removeCasts(assignmentNode.getExpression()); + Node rhs = getTempVarOrNode(rhsExpr); // Ownership transfer to @Owning field. if (lhsElement.getKind() == ElementKind.FIELD) { @@ -1388,6 +1395,66 @@ private void updateObligationsForAssignment( } else if (lhs instanceof LocalVariableNode) { LocalVariableNode lhsVar = (LocalVariableNode) lhs; updateObligationsForPseudoAssignment(obligations, assignmentNode, lhsVar, rhs); + if (isLoopBodyAnalysis) { + handleAssignmentToCollectionElementVariable(obligations, assignmentNode, lhsVar, rhsExpr); + } + } + } + + /** + * In the case of a loop body analysis, this method checks whether the rhs of the assignment + * corresponds to the collection element iterated over, by comparing the AST-trees of the + * collection element and the rhs for structural equality (as defined by + * TreeUtils#sameTree(ExpressionTree, ExpressionTree)). + * + *

If they are determined equal, the lhs variable is added as a resource alias to the + * obligation of the collection element. + * + * @param obligations the set of tracked obligations + * @param node the assignment node to handle + * @param lhsVar the left-hand side for the pseudo-assignment + * @param rhsExpr the right-hand side for the pseudo-assignment, without conversion to a + * temp-variable + */ + private void handleAssignmentToCollectionElementVariable( + Set obligations, AssignmentNode node, LocalVariableNode lhsVar, Node rhsExpr) { + if (!isLoopBodyAnalysis) { + return; + } + Obligation oldObligation = null, newObligation = null; + for (Obligation o : obligations) { + if (oldObligation != null && newObligation != null) break; + for (ResourceAlias alias : o.resourceAliases) { + if ((alias.tree instanceof ExpressionTree) + && (rhsExpr.getTree() instanceof ExpressionTree) + && TreeUtils.sameTree( + (ExpressionTree) alias.tree, (ExpressionTree) rhsExpr.getTree())) { + Set newResourceAliasesForObligation = + new LinkedHashSet<>(o.resourceAliases); + // It is possible to observe assignments to temporary variables, e.g., + // synthetic assignments to ternary expression variables in the CFG. For such + // cases, use the tree associated with the temp var for the resource alias, + // as that is the tree where errors should be reported. + Tree treeForAlias = + // I don't think we need a tempVar here, since the only case where we're interested + // in such an assignment in the loopBodyAnalysis is if the lhs is an actual variable + // to be used as an alias, so we don't care about the case where lhs is a temp-var. + // typeFactory.isTempVar(lhsVar) + // ? typeFactory.getTreeForTempVar(lhsVar) + // : node.getTree(); + node.getTree(); + ResourceAlias aliasForAssignment = + new ResourceAlias(new LocalVariable(lhsVar), treeForAlias); + newResourceAliasesForObligation.add(aliasForAssignment); + oldObligation = o; + newObligation = new Obligation(newResourceAliasesForObligation, o.whenToEnforce); + break; + } + } + } + if (oldObligation != null && newObligation != null) { + obligations.remove(oldObligation); + obligations.add(newObligation); } } @@ -2139,7 +2206,6 @@ private void propagateObligationsToSuccessorBlocks( try { propagateObligationsToSuccessorBlock( - false, cfg, obligations, currentBlock, @@ -2157,8 +2223,6 @@ private void propagateObligationsToSuccessorBlocks( * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, * Deque)} that propagates obligations along a single edge. * - * @param isLoopBodyAnalysis true if part of a loop body analysis (instead of consistency - * analysis) * @param cfg the control flow graph * @param obligations the Obligations for the current block * @param currentBlock the current block @@ -2172,7 +2236,6 @@ private void propagateObligationsToSuccessorBlocks( * analysis and a state without input (unreachable in conventional analysis) is reached */ private void propagateObligationsToSuccessorBlock( - boolean isLoopBodyAnalysis, ControlFlowGraph cfg, Set obligations, Block currentBlock, @@ -2265,8 +2328,9 @@ private void propagateObligationsToSuccessorBlock( // going out of scope: if this is an exit block or there is no information about any // of them in the successor store, all aliases must be going out of scope and a // consistency check should occur. - if (successor.getType() == BlockType.SPECIAL_BLOCK /* special blocks are exit blocks */ - || obligationGoesOutOfScopeBeforeSuccessor) { + if (!isLoopBodyAnalysis + && (successor.getType() == BlockType.SPECIAL_BLOCK /* special blocks are exit blocks */ + || obligationGoesOutOfScopeBeforeSuccessor)) { MustCallAnnotatedTypeFactory mcAtf = cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); @@ -2408,7 +2472,8 @@ private void propagateObligationsToSuccessorBlock( // scope. Set copyOfResourceAliases = new LinkedHashSet<>(obligation.resourceAliases); copyOfResourceAliases.removeIf( - alias -> !aliasInScopeInSuccessor(regularStoreOfSuccessor, alias)); + alias -> + !isLoopBodyAnalysis && !aliasInScopeInSuccessor(regularStoreOfSuccessor, alias)); successorObligations.add( obligation.getReplacement(copyOfResourceAliases, obligation.whenToEnforce)); } @@ -2446,9 +2511,9 @@ private AccumulationStore getStoreForEdgeFromEmptyBlock(Block currentBlock, Bloc /** * Returns true if {@code alias.reference} is definitely in-scope in the successor store: that is, - * there is a value for it in {@code successorStore}. + * there is a value for it in {@code successorStore} or {@code coStore}. * - * @param successorStore the regular store of the successor block + * @param successorStore the regular CalledMethods store of the successor block * @param alias the resource alias to check * @return true if the variable is definitely in scope for the purposes of the consistency * checking algorithm in the successor block from which the store came @@ -2625,6 +2690,11 @@ private static boolean varTrackedInObligations( private void checkMustCall( Obligation obligation, AccumulationStore cmStore, CFStore mcStore, String outOfScopeReason) { + if (isLoopBodyAnalysis) { + // don't report warnings in loop body analysis + return; + } + if (obligation instanceof CollectionObligation) { ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); if (!reportedErrorAliases.contains(firstAlias)) { @@ -3266,7 +3336,6 @@ public void analyzeObligationFulfillingLoop( } else { try { propagateObligationsToSuccessorBlock( - true, cfg, obligations, currentBlock, @@ -3317,7 +3386,6 @@ private Set analyzeTypeOfCollectionElement( // the loop did something weird. Might have reassigned the collection element. // The sound thing to do is return an empty list. // TODO SCK look at this. Why is the collection element obligaiton gone? - // System.out.println("obligation gone for collection element"); return new HashSet<>(); // throw new BugInCF( // "No obligation for collection element " diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 3ecb2ac8dd67..ab4d2573514c 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -784,7 +784,7 @@ public PotentiallyFulfillingLoop( @Override public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this)); + new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); // traverse the cfg to find enhanced-for-loops over collections and perform a // loop-body-analysis. @@ -801,7 +801,6 @@ public void postAnalyze(ControlFlowGraph cfg) { boolean loopContainedInThisMethod = cfg.getNodesCorrespondingToTree(collectionElementTree) != null; if (loopContainedInThisMethod) { - System.out.println("analyzing loop " + potentiallyFulfillingLoop.collectionTree); mustCallConsistencyAnalyzer.analyzeObligationFulfillingLoop( cfg, potentiallyFulfillingLoop); analyzed.add(potentiallyFulfillingLoop); From c1232cbde5bf166f793a4273a3b3c45579070487 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 22:12:38 +0200 Subject: [PATCH 165/374] pattern match fulfilling for-loops on AST --- .../checker/mustcall/MustCallVisitor.java | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 3bbeb0a84d90..ea65d06a8a13 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -1,20 +1,39 @@ package org.checkerframework.checker.mustcall; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.BreakTree; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; 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.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; @@ -23,8 +42,15 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.mustcall.qual.PolyMustCall; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -328,4 +354,370 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { public Void visitAnnotation(AnnotationTree tree, Void p) { return null; } + + /* + * SECTION: syntactically match for-loops that iterate over all elements of a collection on + * the AST + */ + + /** Stores the size variable of the most recent array allocation per array name. */ + private final Map arrayInitializationSize = new HashMap<>(); + + /** + * Checks through pattern-matching whether the loop either: + * + *

    + *
  • initializes entries of an {@code @OwningCollection} + *
  • calls a method on entries of an {@code @OwningCollection} array + *
+ * + * If yes, this is marked in some static datastructures in the + * {@code @MustCallOnElementsAnnotatedTypeFactory} + */ + @Override + public Void visitForLoop(ForLoopTree tree, Void p) { + boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; + if (singleLoopVariable) { + patternMatchFulfillingLoop(tree); + } + return super.visitForLoop(tree, p); + } + + /** + * Checks whether a for-loop potentially fulfills collection obligations of a collection/array and + * marks the loop in case the check is successful. + * + * @param tree forlooptree + */ + private void patternMatchFulfillingLoop(ForLoopTree tree) { + List loopBodyStatementList; + if (tree.getStatement() instanceof BlockTree) { + BlockTree blockT = (BlockTree) tree.getStatement(); + loopBodyStatementList = blockT.getStatements(); + } else { + loopBodyStatementList = Collections.singletonList(tree.getStatement()); + } + StatementTree init = tree.getInitializer().get(0); + ExpressionTree condition = tree.getCondition(); + ExpressionStatementTree update = tree.getUpdate().get(0); + Name identifierInHeader = verifyAllElementsAreCalledOn(init, (BinaryTree) condition, update); + Name iterator = ((VariableTree) init).getName(); + if (identifierInHeader == null || iterator == null) { + return; + } + ExpressionTree collectionElementTree = + preMatchLoop(loopBodyStatementList, identifierInHeader, iterator); + if (collectionElementTree != null) { + // pattern match succeeded, now mark the loop in the respective datastructures + Set conditionNodes = atypeFactory.getNodesForTree(condition); + Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); + Set updateNodes = atypeFactory.getNodesForTree(update.getExpression()); + Block loopConditionBlock = null; + for (Node node : conditionNodes) { + Block blockOfNode = node.getBlock(); + if (blockOfNode != null) { + loopConditionBlock = blockOfNode; + break; + } + } + Block loopUpdateBlock = null; + for (Node node : updateNodes) { + Block blockOfNode = node.getBlock(); + if (blockOfNode != null) { + loopUpdateBlock = blockOfNode; + break; + } + } + Node nodeForCollectionElt = null; + if (collectionEltNodes != null) { + nodeForCollectionElt = collectionEltNodes.iterator().next(); + } + if (loopUpdateBlock == null || loopConditionBlock == null) return; + // add the blocks into a static datastructure in the calledmethodsatf, such that it can + // analyze + // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees + // to the static datastructure in McoeAtf) + assert (loopConditionBlock instanceof SingleSuccessorBlock); + Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); + assert (conditionalBlock instanceof ConditionalBlock); + Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); + RLCCalledMethodsAnnotatedTypeFactory.addPotentiallyFulfillingLoop( + collectionTreeFromExpression(collectionElementTree), + collectionElementTree, + tree.getCondition(), + loopBodyEntryBlock, + loopUpdateBlock, + (ConditionalBlock) conditionalBlock, + nodeForCollectionElt); + } + } + + /** + * Decides for a for-loop header whether the loop iterates over all elements of some array based + * on a pattern-match with one-sided error with the following rules: + * + *
    + *
  • only one loop variable + *
  • initialization must be of the form i = 0 + *
  • condition must be of the form (i < arr.length) or (i < n), where n and arr are + * identifiers and n is effectively final + *
  • update must be prefix or postfix + *
+ * + * Returns: + * + *
    + *
  • null if any rule is violated + *
  • the name of the array if the loop condition is of the form (i < arr.length) + *
  • n if it is of the form (i < n), where n is an identifier. + *
+ * + * @param init the initializer of the loop + * @param condition the loop condition + * @param update the loop update + * @return null if any rule is violated, or the name of the array if the loop condition is of the + * form (i < arr.length) or n if it is of the form (i < n), where n is an identifier. + */ + protected Name verifyAllElementsAreCalledOn( + StatementTree init, BinaryTree condition, ExpressionStatementTree update) { + Tree.Kind updateKind = update.getExpression().getKind(); + if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { + UnaryTree inc = (UnaryTree) update.getExpression(); + // verify update is of form i++ or ++i and init is variable initializer + if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) + return null; + VariableTree initVar = (VariableTree) init; + // verify that intializer is i=0 + if (!(initVar.getInitializer() instanceof LiteralTree) + || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { + return null; + } + // verify that condition is of the form: i < something + if (!(condition.getLeftOperand() instanceof IdentifierTree)) return null; + if (initVar.getName() + != ((IdentifierTree) condition.getLeftOperand()).getName() // i=0 and i statements, Name identifierInHeader, Name iterator) { + AtomicBoolean blockIsIllegal = new AtomicBoolean(false); + final ExpressionTree[] collectionElementTree = {null}; + + TreeScanner scanner = + new TreeScanner() { + @Override + public Void visitUnary(UnaryTree tree, Void p) { + switch (tree.getKind()) { + case PREFIX_DECREMENT: + case POSTFIX_DECREMENT: + case PREFIX_INCREMENT: + case POSTFIX_INCREMENT: + if (nameFromExpression(tree.getExpression()) == iterator) { + blockIsIllegal.set(true); + } + break; + default: + break; + } + return super.visitUnary(tree, p); + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + if (nameFromExpression(tree.getVariable()) == iterator) { + blockIsIllegal.set(true); + } + return super.visitCompoundAssignment(tree, p); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void p) { + Name assignedVariable = nameFromExpression(tree.getVariable()); + if (assignedVariable == iterator || assignedVariable == identifierInHeader) { + blockIsIllegal.set(true); + } + + return super.visitAssignment(tree, p); + } + + @Override + public Void visitBreak(BreakTree bt, Void p) { + blockIsIllegal.set(true); + return super.visitBreak(bt, p); + } + + @Override + public Void visitReturn(ReturnTree rt, Void p) { + blockIsIllegal.set(true); + return super.visitReturn(rt, p); + } + + // check whether corresponds to collection.get(i) + @Override + public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + if (isIthCollectionElement(mit, iterator) + && loopHeaderConsistentWithCollection( + identifierInHeader, nameFromExpression(mit))) { + collectionElementTree[0] = mit; + } + return super.visitMethodInvocation(mit, p); + } + + // check whether corresponds to arr[i] + @Override + public Void visitArrayAccess(ArrayAccessTree aat, Void p) { + boolean isIthArrayElement = nameFromExpression(aat.getIndex()) == iterator; + if (isIthArrayElement + && loopHeaderConsistentWithCollection( + identifierInHeader, nameFromExpression(aat))) { + collectionElementTree[0] = aat; + } + return super.visitArrayAccess(aat, p); + } + }; + + for (StatementTree stmt : statements) { + scanner.scan(stmt, null); + } + if (!blockIsIllegal.get() && collectionElementTree[0] != null) { + return collectionElementTree[0]; + } + return null; + } + + /** + * Get name from an ExpressionTree + * + * @param expr ExpressionTree + * @return Name of the identifier the expression evaluates to or null if it doesn't + */ + protected Name nameFromExpression(ExpressionTree expr) { + if (expr == null) return null; + switch (expr.getKind()) { + case IDENTIFIER: + return ((IdentifierTree) expr).getName(); + case ARRAY_ACCESS: + return nameFromExpression(((ArrayAccessTree) expr).getExpression()); + case MEMBER_SELECT: + Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); + if (elt.getKind() == ElementKind.METHOD || elt.getKind() == ElementKind.FIELD) { + return nameFromExpression(((MemberSelectTree) expr).getExpression()); + } else { + return null; + } + case METHOD_INVOCATION: + return nameFromExpression(((MethodInvocationTree) expr).getMethodSelect()); + default: + return null; + } + } + + /** + * Returns the ExpressionTree of the collection/array in the given expression + * + * @param expr ExpressionTree + * @return the expression evaluates to or null if it doesn't + */ + protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { + switch (expr.getKind()) { + case IDENTIFIER: + return expr; + case ARRAY_ACCESS: + return ((ArrayAccessTree) expr).getExpression(); + case MEMBER_SELECT: + Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); + if (elt.getKind() == ElementKind.METHOD) { + return ((MemberSelectTree) expr).getExpression(); + } else { + return null; + } + case METHOD_INVOCATION: + return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); + default: + return null; + } + } + + /** + * Returns whether the given collection name is consistent with the identifier from the loop + * header. + * + *

That is, either the names are equal, or the identifier from the header is the same variable + * used to initialize the given collection. + * + *

Returns false if any argument is null. + * + * @param idInHeader identifier from loop header + * @param collectionName name of collection + * @return whether the given collection name is consistent with the identifier from the loop + * header. + */ + private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collectionName) { + if (idInHeader == null || collectionName == null) return false; + boolean namesAreEqual = collectionName == idInHeader; + Name initSize = arrayInitializationSize.get(collectionName); + boolean idInHeaderIsSizeOfCollection = initSize != null && initSize == idInHeader; + return namesAreEqual || idInHeaderIsSizeOfCollection; + } + + /** + * Returns whether the given tree is of the form collection.get(i), where i is the given index + * name. + * + * @param tree the tree to check + * @param index the index variable name + * @return whether the given tree is of the form collection.get(index) + */ + private boolean isIthCollectionElement(Tree tree, Name index) { + if (tree == null || index == null) return false; + if (tree.getKind() == Tree.Kind.METHOD_INVOCATION + && index == nameFromExpression(TreeUtils.getIdxForGetCall(tree))) { + MethodInvocationTree mit = (MethodInvocationTree) tree; + ExpressionTree methodSelect = mit.getMethodSelect(); + assert methodSelect.getKind() == Tree.Kind.MEMBER_SELECT + : "method selection of object.get() expected to be memberSelectTree, but is " + + methodSelect.getKind(); + MemberSelectTree mst = (MemberSelectTree) methodSelect; + Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); + return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + } + return false; + } } From 3f75eca6753bf09b5e45ddd49992c523e1c1baf0 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 22 Jun 2025 22:12:50 +0200 Subject: [PATCH 166/374] add normal for loop tests --- .../LoopBodyAnalysisTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 6ede38f91cd5..6aa79be7631f 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -150,6 +150,29 @@ void doCloseFlush(Resource r) { r.flush(); } + void indexForLoop(Resource @OwningCollection [] resources) { + for (int i = 0; i < resources.length; i++) { + resources[i].close(); + resources[i].flush(); + } + } + + // :: error: unfulfilled.collection.obligations + void indexForLoopPartial(Resource @OwningCollection [] resources) { + for (int i = 0; i < resources.length; i++) { + resources[i].close(); + // missing flush + } + } + + void indexForLoopList(@OwningCollection List resources) { + for (int i = 0; i < resources.size(); i++) { + Resource r = resources.get(i); + r.close(); + r.flush(); + } + } + void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} void checkArgIsOCWO(Resource @OwningCollectionWithoutObligation [] arg) {} From 23b66c03971251c86a01797a1a5a292525ca433a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 23 Jun 2025 12:33:41 +0200 Subject: [PATCH 167/374] fix wpi-many caused crash on for-loop patternmatch --- .../checker/mustcall/MustCallVisitor.java | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index ea65d06a8a13..e0dc3b10f29e 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -401,7 +401,7 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { ExpressionTree condition = tree.getCondition(); ExpressionStatementTree update = tree.getUpdate().get(0); Name identifierInHeader = verifyAllElementsAreCalledOn(init, (BinaryTree) condition, update); - Name iterator = ((VariableTree) init).getName(); + Name iterator = getNameFromStatementTree(init); if (identifierInHeader == null || iterator == null) { return; } @@ -501,7 +501,7 @@ protected Name verifyAllElementsAreCalledOn( return null; } if (TreeUtils.isArrayLengthAccess(condition.getRightOperand())) { - return nameFromExpression(condition.getRightOperand()); + return getNameFromExpressionTree(condition.getRightOperand()); } else if ((condition.getRightOperand() instanceof MethodInvocationTree) && TreeUtils.isSizeAccess(condition.getRightOperand())) { ExpressionTree methodSelect = @@ -512,10 +512,10 @@ protected Name verifyAllElementsAreCalledOn( MemberSelectTree mst = (MemberSelectTree) methodSelect; Element elt = TreeUtils.elementFromTree(mst.getExpression()); if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { - return nameFromExpression(mst.getExpression()); + return getNameFromExpressionTree(mst.getExpression()); } } else if (condition.getRightOperand().getKind() == Tree.Kind.IDENTIFIER) { - return nameFromExpression(condition.getRightOperand()); + return getNameFromExpressionTree(condition.getRightOperand()); } } return null; @@ -549,7 +549,7 @@ public Void visitUnary(UnaryTree tree, Void p) { case POSTFIX_DECREMENT: case PREFIX_INCREMENT: case POSTFIX_INCREMENT: - if (nameFromExpression(tree.getExpression()) == iterator) { + if (getNameFromExpressionTree(tree.getExpression()) == iterator) { blockIsIllegal.set(true); } break; @@ -561,7 +561,7 @@ public Void visitUnary(UnaryTree tree, Void p) { @Override public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { - if (nameFromExpression(tree.getVariable()) == iterator) { + if (getNameFromExpressionTree(tree.getVariable()) == iterator) { blockIsIllegal.set(true); } return super.visitCompoundAssignment(tree, p); @@ -569,7 +569,7 @@ public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { @Override public Void visitAssignment(AssignmentTree tree, Void p) { - Name assignedVariable = nameFromExpression(tree.getVariable()); + Name assignedVariable = getNameFromExpressionTree(tree.getVariable()); if (assignedVariable == iterator || assignedVariable == identifierInHeader) { blockIsIllegal.set(true); } @@ -594,7 +594,7 @@ public Void visitReturn(ReturnTree rt, Void p) { public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { if (isIthCollectionElement(mit, iterator) && loopHeaderConsistentWithCollection( - identifierInHeader, nameFromExpression(mit))) { + identifierInHeader, getNameFromExpressionTree(mit))) { collectionElementTree[0] = mit; } return super.visitMethodInvocation(mit, p); @@ -603,10 +603,10 @@ identifierInHeader, nameFromExpression(mit))) { // check whether corresponds to arr[i] @Override public Void visitArrayAccess(ArrayAccessTree aat, Void p) { - boolean isIthArrayElement = nameFromExpression(aat.getIndex()) == iterator; + boolean isIthArrayElement = getNameFromExpressionTree(aat.getIndex()) == iterator; if (isIthArrayElement && loopHeaderConsistentWithCollection( - identifierInHeader, nameFromExpression(aat))) { + identifierInHeader, getNameFromExpressionTree(aat))) { collectionElementTree[0] = aat; } return super.visitArrayAccess(aat, p); @@ -628,22 +628,40 @@ identifierInHeader, nameFromExpression(aat))) { * @param expr ExpressionTree * @return Name of the identifier the expression evaluates to or null if it doesn't */ - protected Name nameFromExpression(ExpressionTree expr) { + protected Name getNameFromExpressionTree(ExpressionTree expr) { if (expr == null) return null; switch (expr.getKind()) { case IDENTIFIER: return ((IdentifierTree) expr).getName(); case ARRAY_ACCESS: - return nameFromExpression(((ArrayAccessTree) expr).getExpression()); + return getNameFromExpressionTree(((ArrayAccessTree) expr).getExpression()); case MEMBER_SELECT: Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); if (elt.getKind() == ElementKind.METHOD || elt.getKind() == ElementKind.FIELD) { - return nameFromExpression(((MemberSelectTree) expr).getExpression()); + return getNameFromExpressionTree(((MemberSelectTree) expr).getExpression()); } else { return null; } case METHOD_INVOCATION: - return nameFromExpression(((MethodInvocationTree) expr).getMethodSelect()); + return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); + default: + return null; + } + } + + /** + * Get name from a {@code StatementTree} + * + * @param expr the {@code StatementTree} + * @return Name of the identifier the expression evaluates to or null if it doesn't + */ + protected Name getNameFromStatementTree(StatementTree expr) { + if (expr == null) return null; + switch (expr.getKind()) { + case VARIABLE: + return ((VariableTree) expr).getName(); + case EXPRESSION_STATEMENT: + return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); default: return null; } @@ -708,7 +726,7 @@ private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collect private boolean isIthCollectionElement(Tree tree, Name index) { if (tree == null || index == null) return false; if (tree.getKind() == Tree.Kind.METHOD_INVOCATION - && index == nameFromExpression(TreeUtils.getIdxForGetCall(tree))) { + && index == getNameFromExpressionTree(TreeUtils.getIdxForGetCall(tree))) { MethodInvocationTree mit = (MethodInvocationTree) tree; ExpressionTree methodSelect = mit.getMethodSelect(); assert methodSelect.getKind() == Tree.Kind.MEMBER_SELECT From 8ae0796aa8fc4963418380e63587c8a4c5692e47 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 23 Jun 2025 12:33:50 +0200 Subject: [PATCH 168/374] add missing @param --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index da9c20c64893..68cc0620fbb7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -706,6 +706,7 @@ public String stringForErrorMessage() { * #analyze(ControlFlowGraph)}. * * @param rlc the resource leak checker + * @param isLoopBodyAnalysis true if this is a loop body analysis */ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAnalysis) { this.cmAtf = From f7c8de930e95c8387d3937653f00d04cc14f3bb1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 14:26:28 +0200 Subject: [PATCH 169/374] use annotatedtypemirror to decide resource collection after defaulting --- ...llectionOwnershipAnnotatedTypeFactory.java | 101 +++++++++++++++++- .../CollectionOwnershipTransfer.java | 7 +- .../MustCallConsistencyAnalyzer.java | 34 +++--- .../resourceleak/ResourceLeakUtils.java | 21 ++++ 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 9a21fea3a0f1..7a7c82d94f12 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -40,6 +40,8 @@ import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; @@ -207,7 +209,9 @@ public void postAnalyze(ControlFlowGraph cfg) { } /** - * Returns whether the given type is a resource collection. + * Returns whether the given type is a resource collection. This overload should be used before + * computation of AnnotatedTypeMirrors is completed, in particular in + * addComputedTypeAnnotations(AnnotatedTypeMirror). * *

That is, whether the given type is: * @@ -225,6 +229,27 @@ public boolean isResourceCollection(TypeMirror t) { return list != null && list.size() > 0; } + /** + * Returns whether the given AST tree is a resource collection. + * + *

That is, whether the given tree is of: + * + *

    + *
  1. An array type, whose component has non-empty MustCall type. + *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall + * type. + *
+ * + * @param tree the tree + * @return whether the tree is a resource collection + */ + public boolean isResourceCollection(Tree tree) { + MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); + AnnotatedTypeMirror treeMcType = mcAtf.getAnnotatedType(tree); + List list = getMustCallValuesOfResourceCollectionComponent(treeMcType); + return list != null && list.size() > 0; + } + /** * If the given type is a collection, this method returns the MustCall values of its elements or * null if there are none or if the given type is not a collection. @@ -238,11 +263,80 @@ public boolean isResourceCollection(TypeMirror t) { * MustCall values of its type variable upper bound if there are any or else null. * * - * @param t the AnnotatedTypeMirror + * @param atm the AnnotatedTypeMirror + * @return if the given type is a collection, returns the MustCall values of its elements or null + * if there are none or if the given type is not a collection. + */ + public List getMustCallValuesOfResourceCollectionComponent(AnnotatedTypeMirror atm) { + if (atm == null) { + return null; + } + boolean isCollectionType = ResourceLeakUtils.isCollection(atm.getUnderlyingType()); + boolean isArrayType = atm.getKind() == TypeKind.ARRAY; + + AnnotatedTypeMirror componentType = null; + if (isArrayType) { + componentType = ((AnnotatedArrayType) atm).getComponentType(); + } else if (isCollectionType) { + List typeArgs = + ((AnnotatedDeclaredType) atm).getTypeArguments(); + if (typeArgs.size() != 0) { + componentType = typeArgs.get(0); + } + } + + if (componentType != null) { + MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); + List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); + return list; + } else { + return null; + } + } + + /** + * If the given tree represents a collection, this method returns the MustCall values of its + * elements or null if there are none or if the given type is not a collection. + * + *

That is: + * + *

    + *
  1. if the given tree is of an array type, this method returns the MustCall values of its + * component type if there are any or else null. + *
  2. if the given tree is of a Java.util.Collection implementation, this method returns the + * MustCall values of its type variable upper bound if there are any or else null. + *
+ * + * @param tree the AST tree + * @return if the given tree represents a collection, returns the MustCall values of its elements + * or null if there are none or if the given type is not a collection. + */ + public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { + MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); + return getMustCallValuesOfResourceCollectionComponent(mcAtf.getAnnotatedType(tree)); + } + + /** + * If the given type is a collection, this method returns the MustCall values of its elements or + * null if there are none or if the given type is not a collection. + * + *

That is: + * + *

    + *
  1. if the given type is an array type, this method returns the MustCall values of its + * component type if there are any or else null. + *
  2. if the given type is a Java.util.Collection implementation, this method returns the + * MustCall values of its type variable upper bound if there are any or else null. + *
+ * + * @param t the TypeMirror * @return if the given type is a collection, returns the MustCall values of its elements or null * if there are none or if the given type is not a collection. */ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) { + if (t == null) { + return null; + } boolean isCollectionType = ResourceLeakUtils.isCollection(t); boolean isArrayType = t.getKind() == TypeKind.ARRAY; @@ -256,9 +350,8 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) } } - MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); - if (componentType != null) { + MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); return list; } else { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 3ae54ca15012..1cbe615935da 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -6,7 +6,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; @@ -139,8 +138,7 @@ private TransferResult transformPotentiallyFulfillingLoop( CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionValue); if (collectionCoType == CollectionOwnershipType.OwningCollection) { List mustCallValuesOfElements = - atypeFactory.getMustCallValuesOfResourceCollectionComponent( - collectionValue.getUnderlyingType()); + atypeFactory.getMustCallValuesOfResourceCollectionComponent(loop.collectionTree); if (loop.getMethods().containsAll(mustCallValuesOfElements)) { elseStore.clearValue(collectionJx); elseStore.insertValue(collectionJx, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); @@ -289,8 +287,7 @@ public TransferResult visitObjectCreation( } CollectionOwnershipType resolvedType = atypeFactory.getCoType(tempVarVal); - TypeMirror javaTypeOfExpr = TreeUtils.elementFromTree(tempVarNode.getTree()).asType(); - if (atypeFactory.isResourceCollection(javaTypeOfExpr)) { + if (atypeFactory.isResourceCollection(node.getTree())) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { store.clearValue(tempVarJx); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 68cc0620fbb7..16236ffc4be0 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -774,7 +774,7 @@ private void addObligationsForOwningCollectionReturn(Set obligations ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), node.getTree()); List mustCallValues = - coAtf.getMustCallValuesOfResourceCollectionComponent(returnCfVal.getUnderlyingType()); + coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree()); if (mustCallValues == null) { throw new BugInCF( "List of MustCall values of component type is null for OwningCollection return value: " @@ -2575,24 +2575,20 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { CFValue paramCfVal = coStore.getValue(JavaExpression.fromVariableTree(param)); CollectionOwnershipType cotype = coAtf.getCoType(paramCfVal); if (cotype == CollectionOwnershipType.OwningCollection) { - List mustCallValues = - coAtf.getMustCallValuesOfResourceCollectionComponent(paramCfVal.getUnderlyingType()); - if (mustCallValues == null) { - throw new BugInCF( - "List of MustCall values of component type is null for OwningCollection parameter: " - + param); - } - for (String mustCallMethod : mustCallValues) { - result.add( - new CollectionObligation( - mustCallMethod, - ImmutableSet.of( - new ResourceAlias( - new LocalVariable(paramElement), - paramElement, - param, - hasMustCallAlias)), - Collections.singleton(MethodExitKind.NORMAL_RETURN))); + List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(param); + if (mustCallValues != null) { + for (String mustCallMethod : mustCallValues) { + result.add( + new CollectionObligation( + mustCallMethod, + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), + paramElement, + param, + hasMustCallAlias)), + Collections.singleton(MethodExitKind.NORMAL_RETURN))); + } } } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index ce9176e45ed1..48372b65a0fc 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -372,6 +372,27 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem return new ArrayList<>(); } + /** + * Returns the list of mustcall obligations for the given {@code AnnotatedTypeMirror} upper bound + * (either the type variable itself if it is concrete or the upper bound if its a wildcard or + * generic). + * + * @param type the {@code AnnotatedTypeMirror} + * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type + * @return the list of mustcall obligations for the upper bound of {@code type}. + */ + public static List getMcValues( + AnnotatedTypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { + AnnotationMirror anno = type.getEffectiveAnnotationInHierarchy(mcAtf.TOP); + if (anno == null) { + return Collections.emptyList(); + } + if (AnnotationUtils.areSameByName(anno, mcAtf.TOP)) { + return Collections.singletonList(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME); + } + return getValuesInAnno(anno, mcAtf.getMustCallValueElement()); + } + /** * Return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation. * From 61c79aefb526cb2848ae73ec9716063777eefee1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 14:44:40 +0200 Subject: [PATCH 170/374] add test cases for new assignment behavior --- .../CollectionOwnershipBasicTyping.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 25298a883c1a..ce88463db33f 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -89,6 +89,21 @@ void testDiamond() { closeElements(col); } + void checkAdvancedDiamond() { + // check that this doesn't create an obligation and defaults to @OwningCollectionBottom + Collection<@MustCall Socket> c = new ArrayList<>(); + checkArgIsBottom(c); + + // these all create obligations + + // :: error: unfulfilled.collection.obligations + Collection c1 = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + Collection<@MustCall("close") Object> c2 = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + Collection<@MustCallUnknown Object> c3 = new ArrayList<>(); + } + void closeElements(@OwningCollection Collection socketCollection) { for (Socket s : socketCollection) { try { @@ -98,6 +113,8 @@ void closeElements(@OwningCollection Collection socketCollection) { } } + void checkArgIsBottom(Collection collection) {} + // these two methods take on unfulfillable obligations and thus throw an error void checkArgIsOwning( From 0e007b15733615680944c9f725a41848474137da Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 15:37:39 +0200 Subject: [PATCH 171/374] fix crash in pattern match of loops --- .../checker/mustcall/MustCallVisitor.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index e0dc3b10f29e..a052c7d2a9e6 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -506,13 +506,12 @@ protected Name verifyAllElementsAreCalledOn( && TreeUtils.isSizeAccess(condition.getRightOperand())) { ExpressionTree methodSelect = ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); - assert methodSelect.getKind() == Tree.Kind.MEMBER_SELECT - : "method selection of object.size() expected to be memberSelectTree, but is " - + methodSelect.getKind(); - MemberSelectTree mst = (MemberSelectTree) methodSelect; - Element elt = TreeUtils.elementFromTree(mst.getExpression()); - if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { - return getNameFromExpressionTree(mst.getExpression()); + if (methodSelect.getKind() == Tree.Kind.MEMBER_SELECT) { + MemberSelectTree mst = (MemberSelectTree) methodSelect; + Element elt = TreeUtils.elementFromTree(mst.getExpression()); + if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { + return getNameFromExpressionTree(mst.getExpression()); + } } } else if (condition.getRightOperand().getKind() == Tree.Kind.IDENTIFIER) { return getNameFromExpressionTree(condition.getRightOperand()); From 0da4598ad398b1971c195aa32498aace456547ef Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 15:38:13 +0200 Subject: [PATCH 172/374] consider iterators collections, but special case for obligation creation --- .../MustCallConsistencyAnalyzer.java | 34 ++++++++++-------- .../resourceleak/ResourceLeakUtils.java | 35 ++++++++++++++----- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 16236ffc4be0..693f7a7df801 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -780,10 +780,12 @@ private void addObligationsForOwningCollectionReturn(Set obligations "List of MustCall values of component type is null for OwningCollection return value: " + node); } - for (String mustCallMethod : mustCallValues) { - obligations.add( - new CollectionObligation( - mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); + if (!ResourceLeakUtils.isIterator(returnCfVal.getUnderlyingType())) { + for (String mustCallMethod : mustCallValues) { + obligations.add( + new CollectionObligation( + mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); + } } } } @@ -2577,17 +2579,19 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { if (cotype == CollectionOwnershipType.OwningCollection) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(param); if (mustCallValues != null) { - for (String mustCallMethod : mustCallValues) { - result.add( - new CollectionObligation( - mustCallMethod, - ImmutableSet.of( - new ResourceAlias( - new LocalVariable(paramElement), - paramElement, - param, - hasMustCallAlias)), - Collections.singleton(MethodExitKind.NORMAL_RETURN))); + if (!ResourceLeakUtils.isIterator(paramCfVal.getUnderlyingType())) { + for (String mustCallMethod : mustCallValues) { + result.add( + new CollectionObligation( + mustCallMethod, + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), + paramElement, + param, + hasMustCallAlias)), + Collections.singleton(MethodExitKind.NORMAL_RETURN))); + } } } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 48372b65a0fc..12ce07302486 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -260,13 +261,13 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno } /** - * Returns whether the given Element is a java.util.Collection type by checking whether the raw - * type of the element is assignable from java.util.Collection. Returns false if element is null, - * or has no valid type. + * Returns whether the given Element is a java.lang.Iterable or java.util.Iterator type by + * checking whether the raw type of the element is assignable from either. Returns false if + * element is null, or has no valid type. * * @param element the element * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given element is a Java.util.Collection type + * @return whether the given element is a Java.lang.Iterable or Java.util.Iterator type */ public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { if (element == null) return false; @@ -276,18 +277,34 @@ public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { } /** - * Returns whether the given {@link TypeMirror} is a java.util.Collection subclass. This is - * determined by getting the class of the TypeMirror and checking whether it is assignable from - * java.util.Collection. + * Returns whether the given {@link TypeMirror} is a java.lang.Iterable or java.util.Iterator + * subclass. This is determined by getting the class of the TypeMirror and checking whether it is + * assignable from either. * * @param type the TypeMirror - * @return whether type is a java.util.Collection + * @return whether type is a java.lang.Iterable or java.util.Iterator */ public static boolean isCollection(TypeMirror type) { if (type == null) return false; Class elementRawType = TypesUtils.getClassFromType(type); if (elementRawType == null) return false; - return Iterable.class.isAssignableFrom(elementRawType); + return Iterable.class.isAssignableFrom(elementRawType) + || Iterator.class.isAssignableFrom(elementRawType); + } + + /** + * Returns whether the given {@link TypeMirror} is a java.util.Iterator subclass. This is + * determined by getting the class of the TypeMirror and checking whether it is assignable from + * java.util.Iterator. + * + * @param type the TypeMirror + * @return whether type is a java.util.Iterator + */ + public static boolean isIterator(TypeMirror type) { + if (type == null) return false; + Class elementRawType = TypesUtils.getClassFromType(type); + if (elementRawType == null) return false; + return Iterator.class.isAssignableFrom(elementRawType); } /** From 7e503e9ae608637c83b9ca4962391b152d79e4d7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 15:38:25 +0200 Subject: [PATCH 173/374] formatting --- .../CollectionOwnershipBasicTyping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index ce88463db33f..7a6fa2983931 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -93,7 +93,7 @@ void checkAdvancedDiamond() { // check that this doesn't create an obligation and defaults to @OwningCollectionBottom Collection<@MustCall Socket> c = new ArrayList<>(); checkArgIsBottom(c); - + // these all create obligations // :: error: unfulfilled.collection.obligations From af5fe4b6ce2fc5e7baae2ab7c7daeed4cb69906e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 17:36:01 +0200 Subject: [PATCH 174/374] add PolyOwningCollection --- .../qual/PolyOwningCollection.java | 19 +++++++++++++++++++ ...llectionOwnershipAnnotatedTypeFactory.java | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java new file mode 100644 index 000000000000..402419340f5c --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.collectionownership.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; +import org.checkerframework.framework.qual.PolymorphicQualifier; + +/** + * A polymorphic qualifier for the Collection-Ownership type system. + * + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(NotOwningCollection.class) +public @interface PolyOwningCollection {} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 7a7c82d94f12..50f466846034 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -23,6 +23,7 @@ import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; @@ -165,6 +166,7 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( Arrays.asList( + PolyOwningCollection.class, NotOwningCollection.class, OwningCollection.class, OwningCollectionWithoutObligation.class, From 95f21705a8d66f0c3498edbd95f980433fa07eaa Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 18:18:40 +0200 Subject: [PATCH 175/374] annotate iterator override in issue6030 --- checker/tests/resourceleak/Issue6030.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak/Issue6030.java b/checker/tests/resourceleak/Issue6030.java index ecf3c5af5179..f269647f9a40 100644 --- a/checker/tests/resourceleak/Issue6030.java +++ b/checker/tests/resourceleak/Issue6030.java @@ -1,6 +1,7 @@ // Test case for https://github.com/typetools/checker-framework/issues/6030 import java.util.*; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; @@ -12,7 +13,7 @@ static class MyScanner> implements CloseableIterato @Owning I iterator; // :: error: missing.creates.mustcall.for - public boolean hasNext() { + public boolean hasNext(@NotOwningCollection MyScanner this) { if (iterator == null) iterator = createIterator(); return iterator.hasNext(); } @@ -21,7 +22,7 @@ public boolean hasNext() { * The @NotOwning annotation is required to be consistent with the superclass implementation. * The return type of Iterator#next is @NotOwning. Soundness is ensured by the RLC for collections. */ - public @NotOwning T next() { + public @NotOwning T next(@NotOwningCollection MyScanner this) { return null; } From 5d795254f26d0bc9e4ad5f5c76d03d2129dc56a0 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 19:40:16 +0200 Subject: [PATCH 176/374] add collection ownership receiver annotation --- checker/tests/resourceleak/Issue6030.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/tests/resourceleak/Issue6030.java b/checker/tests/resourceleak/Issue6030.java index f269647f9a40..082b0003842a 100644 --- a/checker/tests/resourceleak/Issue6030.java +++ b/checker/tests/resourceleak/Issue6030.java @@ -26,7 +26,7 @@ public boolean hasNext(@NotOwningCollection MyScanner this) { return null; } - private I createIterator() { + private I createIterator(@NotOwningCollection MyScanner this) { return null; } From 331931ea13b5fdd13971c81d19e214fc1a04f88e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 24 Jun 2025 22:30:29 +0200 Subject: [PATCH 177/374] default resource collection constructor returns no OCwO --- .../qual/CreatesCollectionObligation.java | 23 +++++++++ ...llectionOwnershipAnnotatedTypeFactory.java | 13 +++-- .../CollectionOwnershipTransfer.java | 50 +++++++++---------- .../MustCallConsistencyAnalyzer.java | 48 ++++++++++++++++-- 4 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java new file mode 100644 index 000000000000..04d9e7f040b5 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.collectionownership.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; + +/** + * A method carrying this annotation creates a {@code CollectionObligation} for the receiver + * collection. If the receiver of a method annotated with this annotation is of type + * {@code @OwningCollectionWithoutObligation}, it is unrefined to {@code @OwningCollection}, and a + * CollectionObligation is created for each {@code @MustCall} method of the type variable of the + * receiver. + * + *

This annotation should only be used on method and constructor declarations of collections, as + * defined by the CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code + * java.util.Iterator} implementations. + */ +@InheritedAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface CreatesCollectionObligation {} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 50f466846034..fc062aab70d4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -433,8 +433,12 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void if (isResourceCollection(returnType.getUnderlyingType())) { AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { - returnType.replaceAnnotation( - CollectionOwnershipAnnotatedTypeFactory.this.OWNINGCOLLECTION); + boolean isConstructor = t.getElement().getKind() == ElementKind.CONSTRUCTOR; + if (isConstructor) { + returnType.replaceAnnotation(OWNINGCOLLECTIONWITHOUTOBLIGATION); + } else { + returnType.replaceAnnotation(OWNINGCOLLECTION); + } } } @@ -442,8 +446,7 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void if (isResourceCollection(paramType.getUnderlyingType())) { AnnotationMirror manualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { - paramType.replaceAnnotation( - CollectionOwnershipAnnotatedTypeFactory.this.NOTOWNINGCOLLECTION); + paramType.replaceAnnotation(NOTOWNINGCOLLECTION); } } } @@ -505,7 +508,7 @@ public CollectionOwnershipTreeAnnotator( @Override public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { if (isResourceCollection(type.getUnderlyingType())) { - type.replaceAnnotation(OWNINGCOLLECTION); + type.replaceAnnotation(OWNINGCOLLECTIONWITHOUTOBLIGATION); } return super.visitNewArray(tree, type); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 1cbe615935da..8d85998cd66a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -7,6 +7,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; +import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; @@ -167,31 +168,29 @@ public TransferResult visitMethodInvocation( ExecutableElement method = node.getTarget().getMethod(); List args = node.getArguments(); res = transferOwnershipForMethodInvocation(method, args, res); - res = transformPotentiallyFulfillingLoop(res, node.getTree()); - // if (!noCreatesMustCallFor) { - // List targetExprs = - // CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( - // n, atypeFactory, atypeFactory); - // for (JavaExpression targetExpr : targetExprs) { - // AnnotationMirror defaultType = - // atypeFactory - // .getAnnotatedType(TypesUtils.getTypeElement(targetExpr.getType())) - // .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - - // if (result.containsTwoStores()) { - // CFStore thenStore = result.getThenStore(); - // lubWithStoreValue(thenStore, targetExpr, defaultType); - - // CFStore elseStore = result.getElseStore(); - // lubWithStoreValue(elseStore, targetExpr, defaultType); - // } else { - // CFStore store = result.getRegularStore(); - // lubWithStoreValue(store, targetExpr, defaultType); - // } - // } - // } + // check whether the method is annotated @CreatesCollectionObligation + ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); + boolean hasCreatesCollectionObligation = + atypeFactory.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; + if (hasCreatesCollectionObligation) { + CFStore coStore = res.getRegularStore(); + Node receiverNode = node.getTarget().getReceiver(); + JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); + CFValue receiverCoType = null; + try { + receiverCoType = coStore.getValue(receiverJx); + } catch (Exception e) { + return res; + } + if (atypeFactory.getCoType(receiverCoType) + == CollectionOwnershipType.OwningCollectionWithoutObligation) { + coStore.clearValue(receiverJx); + coStore.insertValue(receiverJx, atypeFactory.OWNINGCOLLECTION); + } + } + return res; } @@ -291,9 +290,10 @@ public TransferResult visitObjectCreation( boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { store.clearValue(tempVarJx); - store.insertValue(tempVarJx, atypeFactory.OWNINGCOLLECTION); + store.insertValue(tempVarJx, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); resultValue = - analysis.createSingleAnnotationValue(atypeFactory.OWNINGCOLLECTION, node.getType()); + analysis.createSingleAnnotationValue( + atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION, node.getType()); } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 693f7a7df801..822d5ee27c26 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -41,6 +41,7 @@ import org.checkerframework.checker.calledmethods.qual.CalledMethods; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; +import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -533,9 +534,11 @@ public CollectionObligation( * Create a CollectionObligation from an Obligation * * @param obligation the obligation to create a CollectionObligation from + * @param mustCallMethod the method that must be called on the elements of the collection */ - private CollectionObligation(Obligation obligation) { + private CollectionObligation(Obligation obligation, String mustCallMethod) { super(obligation.resourceAliases, obligation.whenToEnforce); + this.mustCallMethod = mustCallMethod; } /** @@ -544,10 +547,11 @@ private CollectionObligation(Obligation obligation) { * * @param tree the tree from which the CollectionObligation is to be created. Must be * ExpressionTree or VariableTree. + * @param mustCallMethod the method that must be called on the elements of the collection * @return a CollectionObligation derived from the given tree */ - public static CollectionObligation fromTree(Tree tree) { - return new CollectionObligation(Obligation.fromTree(tree)); + public static CollectionObligation fromTree(Tree tree, String mustCallMethod) { + return new CollectionObligation(Obligation.fromTree(tree), mustCallMethod); } /** @@ -791,6 +795,40 @@ private void addObligationsForOwningCollectionReturn(Set obligations } } + /** + * Adds {@code CollectionObligation}s if the method is annotated + * {@code @CreatesCollectionObligation} and the receiver is currently + * {@code @OwningCollectionWithoutObligation}. + * + * @param obligations the set of tracked obligations + * @param node the method invocation node + */ + private void addObligationsForCreatesCollectionObligationAnno( + Set obligations, MethodInvocationNode node) { + ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); + boolean hasCreatesCollectionObligation = + coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; + if (hasCreatesCollectionObligation) { + CFStore coStore = coAtf.getStoreBefore(node); + Node receiverNode = node.getTarget().getReceiver(); + JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); + CFValue receiverCoType = null; + try { + receiverCoType = coStore.getValue(receiverJx); + } catch (Exception e) { + return; + } + if (coAtf.getCoType(receiverCoType) + == CollectionOwnershipType.OwningCollectionWithoutObligation) { + List mustCallValues = + coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); + for (String mustCallMethod : mustCallValues) { + obligations.add(CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); + } + } + } + } + /** * Update a set of Obligations to account for a method or constructor invocation. * @@ -815,6 +853,10 @@ private void updateObligationsForInvocation( addObligationsForOwningCollectionReturn(obligations, node); + if (node instanceof MethodInvocationNode) { + addObligationsForCreatesCollectionObligationAnno(obligations, (MethodInvocationNode) node); + } + if (!shouldTrackInvocationResult(obligations, node, false)) { return; } From 123f26d8b375f265964692cc83f1e9eceffceae0 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 00:59:01 +0200 Subject: [PATCH 178/374] fix return rule for resource collections --- .../MustCallConsistencyAnalyzer.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 822d5ee27c26..9348b0fd4743 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1346,6 +1346,12 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { // not be transferred. MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); + boolean returnTypeHasManualNocAnno = + coAtf.getCoType(new HashSet<>(executableElement.getReturnType().getAnnotationMirrors())) + == CollectionOwnershipType.NotOwningCollection; + if (returnTypeHasManualNocAnno) { + return false; + } return !cmAtf.hasNotOwning(executableElement); } return false; @@ -2251,7 +2257,6 @@ private void propagateObligationsToSuccessorBlocks( try { propagateObligationsToSuccessorBlock( - cfg, obligations, currentBlock, successorAndExceptionType.first, @@ -2268,7 +2273,6 @@ private void propagateObligationsToSuccessorBlocks( * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, * Deque)} that propagates obligations along a single edge. * - * @param cfg the control flow graph * @param obligations the Obligations for the current block * @param currentBlock the current block * @param successor a successor of the current block @@ -2281,7 +2285,6 @@ private void propagateObligationsToSuccessorBlocks( * analysis and a state without input (unreachable in conventional analysis) is reached */ private void propagateObligationsToSuccessorBlock( - ControlFlowGraph cfg, Set obligations, Block currentBlock, Block successor, @@ -2349,6 +2352,7 @@ private void propagateObligationsToSuccessorBlock( AccumulationStore regularStoreOfSuccessor = input.getRegularStore(); for (Obligation obligation : obligations) { + if (isElseEdgeOfFulfillingLoop) { if (obligation instanceof CollectionObligation) { String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; @@ -2495,20 +2499,6 @@ private void propagateObligationsToSuccessorBlock( exceptionType == null ? MethodExitKind.NORMAL_RETURN : MethodExitKind.EXCEPTIONAL_EXIT; if (obligation.whenToEnforce.contains(exitKind)) { checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); - } else if (exitKind == MethodExitKind.NORMAL_RETURN) { - // check normal returns where return type is @NotOwningCollection. - UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); - if (underlyingAST instanceof UnderlyingAST.CFGMethod) { - MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); - ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); - boolean returnTypeHasManualNocAnno = - coAtf.getCoType( - new HashSet<>(executableElement.getReturnType().getAnnotationMirrors())) - == CollectionOwnershipType.NotOwningCollection; - if (returnTypeHasManualNocAnno) { - checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); - } - } } } else { // In this case, there is info in the successor store about some alias in the @@ -3379,7 +3369,6 @@ public void analyzeObligationFulfillingLoop( } else { try { propagateObligationsToSuccessorBlock( - cfg, obligations, currentBlock, successorAndExceptionType.first, From d9d7feb39b2450c5ea930dd8a044a0a0a4bd72f5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 00:59:27 +0200 Subject: [PATCH 179/374] fix tests to account for enhanced precision of new defaults --- .../CollectionOwnershipBasicTyping.java | 18 +++++++++++++----- .../CollectionOwnershipDefaults.java | 1 - .../LoopBodyAnalysisTest.java | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 7a6fa2983931..8b0c01158629 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -40,8 +40,10 @@ Collection checkReturn(List list) { // do in this case. @NotOwningCollection Collection checkIllegalNotOwningReturn() { + List l = new ArrayList<>(); // :: error: unfulfilled.collection.obligations - return new ArrayList<>(); + l.add(new Socket()); + return l; } // this is the correct version of above. It passes ownership to another method first, @@ -65,8 +67,9 @@ void checkUnrefinement() { void testAssignmentTransfersOwnership() { // col is overwritten and its obligation never fulfilled or passed on - // :: error: unfulfilled.collection.obligations Collection col = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + col.add(new Socket()); Collection col2 = new ArrayList<>(); // col : @OwningCollection, col2 : @OwningCollection col = col2; @@ -84,6 +87,7 @@ void testAssignmentTransfersOwnership() { */ void testDiamond() { Collection col = new ArrayList<>(); + col.add(new Socket()); // :: error: argument checkArgIsOCwoO(col); closeElements(col); @@ -96,12 +100,16 @@ void checkAdvancedDiamond() { // these all create obligations - // :: error: unfulfilled.collection.obligations Collection c1 = new ArrayList<>(); - // :: error: unfulfilled.collection.obligations Collection<@MustCall("close") Object> c2 = new ArrayList<>(); - // :: error: unfulfilled.collection.obligations Collection<@MustCallUnknown Object> c3 = new ArrayList<>(); + c.add(new Socket()); + // :: error: unfulfilled.collection.obligations + c1.add(new Socket()); + // :: error: unfulfilled.collection.obligations + c2.add(new Socket()); + // :: error: unfulfilled.collection.obligations + c3.add(new Socket()); } void closeElements(@OwningCollection Collection socketCollection) { diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index ead9c3d2d904..f27afad94eed 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -80,7 +80,6 @@ List overrideReturnType(List list) { void overrideReturnTypeClient() { // the arraylist passed to the method is never closed. The ownership is not passed // to overrideReturnType(), since parameters are @NotOwningCollection by default - // :: error: unfulfilled.collection.obligations List notOwninglist = overrideReturnType(new ArrayList()); List owninglist = identity(new ArrayList()); diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 6aa79be7631f..8a2a6d5ce28d 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -72,8 +72,8 @@ void partialSatisfyCollectionShouldError(@OwningCollection Collection } void multipleMustCallPartial() { - // :: error: unfulfilled.collection.obligations List l = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations l.add(new Resource()); for (Resource r : l) { r.close(); From ee95caac4f62a1fbc061cd135cd0fbd014d1db0d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 13:53:05 +0200 Subject: [PATCH 180/374] add CollectionOwnershipStore and Analysis --- .../CollectionOwnershipAnalysis.java | 49 +++++++ ...llectionOwnershipAnnotatedTypeFactory.java | 44 ++++-- .../CollectionOwnershipStore.java | 53 +++++++ .../CollectionOwnershipTransfer.java | 130 ++++++++---------- .../MustCallConsistencyAnalyzer.java | 7 +- 5 files changed, 196 insertions(+), 87 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java new file mode 100644 index 000000000000..89501e0908ae --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -0,0 +1,49 @@ +package org.checkerframework.checker.collectionownership; + +import javax.lang.model.type.TypeMirror; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; + +/** + * The analysis class for the collection ownership type system. + * + *

This class extends {@link CFAbstractAnalysis} so that {@link CollectionOwnershipStore} is used + * rather than {@link CFStore}. + */ +public class CollectionOwnershipAnalysis + extends CFAbstractAnalysis { + + /** + * Creates a new {@link CollectionOwnershipAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public CollectionOwnershipAnalysis( + BaseTypeChecker checker, CollectionOwnershipAnnotatedTypeFactory factory) { + super(checker, factory); + } + + @Override + public CollectionOwnershipTransfer createTransferFunction() { + return new CollectionOwnershipTransfer(this, (CollectionOwnershipChecker) checker); + } + + @Override + public CollectionOwnershipStore createEmptyStore(boolean sequentialSemantics) { + return new CollectionOwnershipStore(this, sequentialSemantics); + } + + @Override + public CollectionOwnershipStore createCopiedStore(CollectionOwnershipStore s) { + return new CollectionOwnershipStore(this, s); + } + + @Override + public CFValue createAbstractValue(AnnotationMirrorSet annotations, TypeMirror underlyingType) { + return defaultCreateAbstractValue(this, annotations, underlyingType); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index fc062aab70d4..f94f89bcf51b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -32,26 +32,31 @@ import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the Collection Ownership Checker. */ -public class CollectionOwnershipAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class CollectionOwnershipAnnotatedTypeFactory + extends GenericAnnotatedTypeFactory< + CFValue, + CollectionOwnershipStore, + CollectionOwnershipTransfer, + CollectionOwnershipAnalysis> { /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -182,10 +187,11 @@ protected Set> createSupportedTypeQualifiers() { * successor, succ * @param first a block * @param succ first's successor - * @return the appropriate CFStore, populated with MustCall annotations, from the results of - * running dataflow + * @return the appropriate CollectionOwnershipStore, populated with MustCall annotations, from the + * results of running dataflow */ - public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ) { + public CollectionOwnershipStore getStoreForBlock( + boolean afterFirstStore, Block first, Block succ) { return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); } @@ -455,6 +461,27 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void } } + /* + * Defaults resource collection fields within methods of the class to @OwningCollection. + */ + @Override + protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean iUseFlow) { + super.addComputedTypeAnnotations(tree, type, iUseFlow); + + if (type.getKind() == TypeKind.DECLARED) { + Element elt = TreeUtils.elementFromTree(tree); + if (elt != null) { + boolean isField = elt.getKind() == ElementKind.FIELD; + if (isField && isResourceCollection(type.getUnderlyingType())) { + AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); + if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { + type.replaceAnnotation(OWNINGCOLLECTION); + } + } + } + } + } + /* * Default resource collection fields to @OwningCollection and resource collection parameters to * @NotOwningCollection (inside the method). @@ -514,8 +541,3 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { } } } - - // @Override - // protected QualifierPolymorphism createQualifierPolymorphism() { - // return new MustCallQualifierPolymorphism(processingEnv, this); - // } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java new file mode 100644 index 000000000000..4c08c46e0346 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -0,0 +1,53 @@ +package org.checkerframework.checker.collectionownership; + +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; + +/** + * The Lock Store behaves like CFAbstractStore but keeps @OwningCollection fields in the store. This + * is justified by the strict access rules of such fields. Keeping the field in the store is + * required for verifying the postcondition annotation {@link CollectionFieldDestructor}. + */ +public class CollectionOwnershipStore extends CFAbstractStore { + + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + public CollectionOwnershipStore( + CollectionOwnershipAnalysis analysis, boolean sequentialSemantics) { + super(analysis, sequentialSemantics); + this.atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); + } + + /** Copy constructor. */ + public CollectionOwnershipStore( + CollectionOwnershipAnalysis analysis, + CFAbstractStore other) { + super(other); + this.atypeFactory = ((CollectionOwnershipStore) other).atypeFactory; + } + + /* + * Keep {@code OwningCollection} fields in the store. + */ + @Override + protected CFValue newFieldValueAfterMethodCall( + FieldAccess fieldAccess, + GenericAnnotatedTypeFactory atf, + CFValue value) { + CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value); + if (superResult == null) { + if (atypeFactory.isResourceCollection(fieldAccess.getField().asType())) { + switch (atypeFactory.getCoType(value)) { + case OwningCollection: + case OwningCollectionWithoutObligation: + return value; + default: + return superResult; + } + } + } + return superResult; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 8d85998cd66a..44d424bece49 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -23,9 +23,7 @@ import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; -import org.checkerframework.framework.flow.CFAnalysis; -import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFAbstractTransfer; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -35,7 +33,8 @@ * temporary variables for expressions (which allow those expressions to have refined information in * the store, which the consistency checker can use). */ -public class CollectionOwnershipTransfer extends CFTransfer { +public class CollectionOwnershipTransfer + extends CFAbstractTransfer { /** The type factory. */ private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; @@ -48,18 +47,19 @@ public class CollectionOwnershipTransfer extends CFTransfer { * * @param analysis the analysis */ - public CollectionOwnershipTransfer(CFAnalysis analysis) { + public CollectionOwnershipTransfer( + CollectionOwnershipAnalysis analysis, CollectionOwnershipChecker checker) { super(analysis); atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); - mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(atypeFactory); + mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); } @Override - public TransferResult visitAssignment( - AssignmentNode node, TransferInput in) { - TransferResult res = super.visitAssignment(node, in); + public TransferResult visitAssignment( + AssignmentNode node, TransferInput in) { + TransferResult res = super.visitAssignment(node, in); - CFStore store = res.getRegularStore(); + CollectionOwnershipStore store = res.getRegularStore(); Node lhs = node.getTarget(); lhs = getNodeOrTempVar(lhs); @@ -108,7 +108,8 @@ public TransferResult visitAssignment( // transformWriteToOwningCollection(arrayJx, arrayExpression, node.getExpression(), // store); // } - return new RegularTransferResult(res.getResultValue(), store); + return new RegularTransferResult( + res.getResultValue(), store); } /** @@ -122,12 +123,12 @@ public TransferResult visitAssignment( * collection-obligation-fulfilling loop * @return the resulting transfer result */ - private TransferResult transformPotentiallyFulfillingLoop( - TransferResult res, Tree tree) { + private TransferResult transformPotentiallyFulfillingLoop( + TransferResult res, Tree tree) { PotentiallyFulfillingLoop loop = CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); if (loop != null) { - CFStore elseStore = res.getElseStore(); + CollectionOwnershipStore elseStore = res.getElseStore(); JavaExpression collectionJx = JavaExpression.fromTree(loop.collectionTree); CFValue collectionValue = null; try { @@ -152,16 +153,16 @@ private TransferResult transformPotentiallyFulfillingLoop( } @Override - public TransferResult visitLessThan( - LessThanNode node, TransferInput in) { - TransferResult res = super.visitLessThan(node, in); + public TransferResult visitLessThan( + LessThanNode node, TransferInput in) { + TransferResult res = super.visitLessThan(node, in); return transformPotentiallyFulfillingLoop(res, node.getTree()); } @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode node, TransferInput in) { - TransferResult res = super.visitMethodInvocation(node, in); + public TransferResult visitMethodInvocation( + MethodInvocationNode node, TransferInput in) { + TransferResult res = super.visitMethodInvocation(node, in); updateStoreWithTempVar(res, node); @@ -175,7 +176,7 @@ public TransferResult visitMethodInvocation( boolean hasCreatesCollectionObligation = atypeFactory.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; if (hasCreatesCollectionObligation) { - CFStore coStore = res.getRegularStore(); + CollectionOwnershipStore coStore = res.getRegularStore(); Node receiverNode = node.getTarget().getReceiver(); JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); CFValue receiverCoType = null; @@ -203,11 +204,13 @@ public TransferResult visitMethodInvocation( * @param res the transfer result so far * @return the updated transfer result */ - private TransferResult transferOwnershipForMethodInvocation( - ExecutableElement method, List args, TransferResult res) { + private TransferResult transferOwnershipForMethodInvocation( + ExecutableElement method, + List args, + TransferResult res) { List params = method.getParameters(); - CFStore store = res.getRegularStore(); + CollectionOwnershipStore store = res.getRegularStore(); for (int i = 0; i < Math.min(args.size(), params.size()); i++) { VariableElement param = params.get(i); Node arg = args.get(i); @@ -236,30 +239,15 @@ private TransferResult transferOwnershipForMethodInvocation( } } } - return new RegularTransferResult(res.getResultValue(), store); + return new RegularTransferResult( + res.getResultValue(), store); } - // /** - // * Computes the LUB of the current value in the store for expr, if it exists, and defaultType. - // * Inserts that LUB into the store as the new value for expr. - // * - // * @param store a CFStore - // * @param expr an expression that might be in the store - // * @param defaultType the default type of the expression's static type - // */ - // private void lubWithStoreValue(CFStore store, JavaExpression expr, AnnotationMirror - // defaultType) { - // CFValue value = store.getValue(expr); - // CFValue defaultTypeAsCFValue = - // analysis.createSingleAnnotationValue(defaultType, expr.getType()); - // CFValue newValue = defaultTypeAsCFValue.leastUpperBound(value); - // store.replaceValue(expr, newValue); - // } - @Override - public TransferResult visitObjectCreation( - ObjectCreationNode node, TransferInput input) { - TransferResult result = super.visitObjectCreation(node, input); + public TransferResult visitObjectCreation( + ObjectCreationNode node, TransferInput input) { + TransferResult result = + super.visitObjectCreation(node, input); updateStoreWithTempVar(result, node); ExecutableElement constructor = TreeUtils.elementFromUse(node.getTree()); @@ -273,7 +261,7 @@ public TransferResult visitObjectCreation( // resource collections with no type variables, and if they are @Bottom, they are // unrefined to @OwningCollection. Change the type of both the type var and the computed // expression itself. - CFStore store = result.getRegularStore(); + CollectionOwnershipStore store = result.getRegularStore(); CFValue resultValue = result.getResultValue(); Node tempVarNode = getNodeOrTempVar(node); JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); @@ -297,23 +285,9 @@ public TransferResult visitObjectCreation( } } - return new RegularTransferResult(resultValue, store); + return new RegularTransferResult(resultValue, store); } - // TODO sck: I think that I can use the temp var management from MC checker and don't - // need to replicate that in the COatf. If that turns out to not work, uncomment this - // @Override - // public TransferResult visitSwitchExpressionNode( - // SwitchExpressionNode node, TransferInput input) { - // TransferResult result = super.visitSwitchExpressionNode(node, input); - // if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { - // // Add the synthetic variable created during CFG construction to the temporary - // // variable map (rather than creating a redundant temp var) - // atypeFactory.tempVars.put(node.getTree(), node.getSwitchExpressionVar()); - // } - // return result; - // } - /** * This method either creates or looks up the temp var t for node, and then updates the store to * give t the same type as {@code node}. @@ -321,31 +295,41 @@ public TransferResult visitObjectCreation( * @param node the node to be assigned to a temporary variable * @param result the transfer result containing the store to be modified */ - public void updateStoreWithTempVar(TransferResult result, Node node) { + public void updateStoreWithTempVar( + TransferResult result, Node node) { // Must-call obligations on primitives are not supported. if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { LocalVariableNode temp = mcAtf.getTempVar(node); if (temp != null) { - // atypeFactory.addTempVar(temp, node.getTree()); JavaExpression localExp = JavaExpression.fromNode(temp); AnnotationMirror anm = atypeFactory .getAnnotatedType(node.getTree()) .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - insertIntoStores(result, localExp, anm == null ? atypeFactory.TOP : anm); - // if (anm == null) { - // anm = atypeFactory.TOP; - // } - // if (result.containsTwoStores()) { - // result.getThenStore().insertValue(localExp, anm); - // result.getElseStore().insertValue(localExp, anm); - // } else { - // result.getRegularStore().insertValue(localExp, anm); - // } + insertInStores(result, localExp, anm == null ? atypeFactory.TOP : anm); } } } + /** + * Inserts newAnno as the value into all stores (conditional or not) in the result for node. + * + * @param result the TransferResult holding the stores to modify + * @param target the receiver whose value should be modified + * @param newAnno the new value + */ + protected static void insertInStores( + TransferResult result, + JavaExpression target, + AnnotationMirror newAnno) { + if (result.containsTwoStores()) { + result.getThenStore().insertValue(target, newAnno); + result.getElseStore().insertValue(target, newAnno); + } else { + result.getRegularStore().insertValue(target, newAnno); + } + } + /** * Removes casts from {@code node} and returns the temp-var corresponding to it if it exists or * else {@code node} with removed casts. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 9348b0fd4743..74a899e33d84 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -41,6 +41,7 @@ import org.checkerframework.checker.calledmethods.qual.CalledMethods; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; +import org.checkerframework.checker.collectionownership.CollectionOwnershipStore; import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; @@ -771,7 +772,7 @@ public void analyze(ControlFlowGraph cfg) { private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); if (tmpVar != null) { - CFStore coStore = coAtf.getStoreAfter(node); + CollectionOwnershipStore coStore = coAtf.getStoreAfter(node); CFValue returnCfVal = coStore.getValue(JavaExpression.fromNode(tmpVar)); CollectionOwnershipType cotype = coAtf.getCoType(returnCfVal); if (cotype == CollectionOwnershipType.OwningCollection) { @@ -809,7 +810,7 @@ private void addObligationsForCreatesCollectionObligationAnno( boolean hasCreatesCollectionObligation = coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; if (hasCreatesCollectionObligation) { - CFStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); Node receiverNode = node.getTarget().getReceiver(); JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); CFValue receiverCoType = null; @@ -2584,7 +2585,7 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { if (cfg.getUnderlyingAST().getKind() == Kind.METHOD) { MethodTree method = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); Set result = new LinkedHashSet<>(1); - CFStore coStore = coAtf.getRegularStore(cfg.getEntryBlock()); + CollectionOwnershipStore coStore = coAtf.getRegularStore(cfg.getEntryBlock()); if (coStore == null) { throw new BugInCF("Cannot get initial store for " + cfg); } From ee0ce6d609eb39b872b36f76a75cf33024456689 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 13:53:20 +0200 Subject: [PATCH 181/374] add collectionfielddestructor postcon anno --- .../qual/CollectionFieldDestructor.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java new file mode 100644 index 000000000000..f7966b0908d6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java @@ -0,0 +1,38 @@ +package org.checkerframework.checker.collectionownership.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; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; + +/** + * Indicates that the given resource collection fields are destructed within this method, i.e. this + * method calls all required methods on their elements. + * + *


+ *  {@literal @}CollectionFieldDestructor("socketList")
+ *  void close() {
+ *    for (Socket s : this.socketList) {
+ *      try {
+ *        s.close();
+ *      } catch (Exception e) {}
+ *    }
+ *  }
+ * 
+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@PostconditionAnnotation(qualifier = OwningCollectionWithoutObligation.class) +@InheritedAnnotation +public @interface CollectionFieldDestructor { + /** + * Returns the resource collection field whose collection obligation the destructor fulfills. + * + * @return the resource collection field whose collection obligation the destructor fulfills + */ + String[] value(); +} From 12ae8fc8277af20891a8178484a7b6beab1b0d35 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 13:54:58 +0200 Subject: [PATCH 182/374] doc --- .../collectionownership/CollectionOwnershipStore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 4c08c46e0346..383d0a8dafcc 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -6,9 +6,9 @@ import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; /** - * The Lock Store behaves like CFAbstractStore but keeps @OwningCollection fields in the store. This - * is justified by the strict access rules of such fields. Keeping the field in the store is - * required for verifying the postcondition annotation {@link CollectionFieldDestructor}. + * The CollectionOwnership Store behaves like CFAbstractStore but keeps @OwningCollection fields in + * the store. This is justified by the strict access rules of such fields. Keeping the field in the + * store is required for verifying the postcondition annotation {@link CollectionFieldDestructor}. */ public class CollectionOwnershipStore extends CFAbstractStore { From ce34a108abf668bdaa85dde77594af2eb7541175 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 15:29:02 +0200 Subject: [PATCH 183/374] add support for @OC fields --- ...llectionOwnershipAnnotatedTypeFactory.java | 20 ++ .../CollectionOwnershipVisitor.java | 286 +++++++----------- .../collectionownership/messages.properties | 1 + .../CollectionOwnershipDefaults.java | 12 - 4 files changed, 137 insertions(+), 182 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f94f89bcf51b..1a8e1369018e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -6,6 +6,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -14,11 +15,13 @@ 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.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; @@ -76,6 +79,10 @@ public class CollectionOwnershipAnnotatedTypeFactory */ public final AnnotationMirror BOTTOM; + /** The value element of the {@code @}{@link CollectionFieldDestructor} annotation. */ + public final ExecutableElement collectionFieldDestructorValueElement = + TreeUtils.getMethod(CollectionFieldDestructor.class, "value", 0, processingEnv); + /** * Enum for the types in the hierarchy. Combined with a few utility methods to get the right enum * value from various sources, this is a convenient interface to deal with annotations in this @@ -378,6 +385,19 @@ public CollectionOwnershipType getCoType(CFValue val) { return val == null ? CollectionOwnershipType.None : getCoType(val.getAnnotations()); } + /** + * Utility method to get the {@code CollectionOwnershipType} that the given tree has. + * + * @param tree the tree + * @return the {@code CollectionOwnershipType} that the given tree has. + */ + public CollectionOwnershipType getCoType(Tree tree) { + AnnotatedTypeMirror atm = getAnnotatedType(tree); + return atm == null + ? CollectionOwnershipType.None + : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + } + /** * Utility method to extract the {@code CollectionOwnershipType} from a collection of {@code * AnnotationMirror}s. diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index d5a0d095f2d0..0e6e8cbee6b5 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -1,13 +1,27 @@ package org.checkerframework.checker.collectionownership; +import com.sun.source.tree.VariableTree; +import java.util.List; 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.VariableElement; +import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; /** * The visitor for the Collection Ownership Checker. This visitor is similar to BaseTypeVisitor, but @@ -64,179 +78,111 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { return new AnnotationMirrorSet(atypeFactory.BOTTOM); } - // TODO maybe check contravariance for parameters and covariance for return types here - // (and invariance for fields) - // @Override - // protected boolean validateType(Tree tree, AnnotatedTypeMirror type) { - // if (TreeUtils.isClassTree(tree)) { - // TypeElement classEle = TreeUtils.elementFromDeclaration((ClassTree) tree); - // // If no @InheritableMustCall annotation is written here, `getDeclAnnotation()` gets one - // // from stub files and supertypes. - // AnnotationMirror anyInheritableMustCall = - // atypeFactory.getDeclAnnotation(classEle, InheritableMustCall.class); - // // An @InheritableMustCall annotation that is directly present. - // AnnotationMirror directInheritableMustCall = - // AnnotationUtils.getAnnotationByClass( - // classEle.getAnnotationMirrors(), InheritableMustCall.class); - // if (anyInheritableMustCall == null) { - // if (!ElementUtils.isFinal(classEle)) { - // // There is no @InheritableMustCall annotation on this or any superclass and - // // this is a non-final class. - // // If an explicit @MustCall annotation is present, issue a warning suggesting - // // that @InheritableMustCall is probably what the programmer means, for - // // usability. - // if (atypeFactory.getDeclAnnotation(classEle, MustCall.class) != null) { - // checker.reportWarning( - // tree, "mustcall.not.inheritable", ElementUtils.getQualifiedName(classEle)); - // } - // } - // } else { - // // There is an @InheritableMustCall annotation on this, on a superclass, or in an - // // annotation file. - // // There are two possible problems: - // // 1. There is an inconsistent @MustCall on this. - // // 2. There is an explicit @InheritableMustCall here, and it is inconsistent with - // // an @InheritableMustCall annotation on a supertype. - - // // Check for problem 1. - // AnnotationMirror explicitMustCall = - // atypeFactory.fromElement(classEle).getPrimaryAnnotation(); - // if (explicitMustCall != null) { - // // There is a @MustCall annotation here. - - // List inheritableMustCallVal = - // AnnotationUtils.getElementValueArray( - // anyInheritableMustCall, - // atypeFactory.inheritableMustCallValueElement, - // String.class, - // emptyStringList); - // AnnotationMirror inheritedMCAnno = atypeFactory.createMustCall(inheritableMustCallVal); - - // // Issue an error if there is an inconsistent, user-written @MustCall annotation - // // here. - // AnnotationMirror effectiveMCAnno = type.getPrimaryAnnotation(); - // TypeMirror tm = type.getUnderlyingType(); - // if (effectiveMCAnno != null - // && !qualHierarchy.isSubtypeShallow(inheritedMCAnno, effectiveMCAnno, tm)) { - - // checker.reportError( - // tree, - // "inconsistent.mustcall.subtype", - // ElementUtils.getQualifiedName(classEle), - // effectiveMCAnno, - // anyInheritableMustCall); - // return false; - // } - // } - - // // Check for problem 2. - // if (directInheritableMustCall != null) { - - // // `inheritedImcs` is inherited @InheritableMustCall annotations. - // List inheritedImcs = new ArrayList<>(); - // for (TypeElement elt : ElementUtils.getDirectSuperTypeElements(classEle, elements)) { - // AnnotationMirror imc = atypeFactory.getDeclAnnotation(elt, - // InheritableMustCall.class); - // if (imc != null) { - // inheritedImcs.add(imc); - // } - // } - // if (!inheritedImcs.isEmpty()) { - // // There is an inherited @InheritableMustCall annotation, in addition to the - // // one written explicitly here. - // List inheritedMustCallVal = new ArrayList<>(); - // for (AnnotationMirror inheritedImc : inheritedImcs) { - // inheritedMustCallVal.addAll( - // AnnotationUtils.getElementValueArray( - // inheritedImc, atypeFactory.inheritableMustCallValueElement, String.class)); - // } - // AnnotationMirror inheritedMCAnno = atypeFactory.createMustCall(inheritedMustCallVal); - - // AnnotationMirror effectiveMCAnno = type.getPrimaryAnnotation(); - - // TypeMirror tm = type.getUnderlyingType(); + @Override + public Void visitVariable(VariableTree tree, Void p) { + VariableElement varElement = TreeUtils.elementFromDeclaration(tree); + + if (varElement.getKind().isField() && atypeFactory.isResourceCollection(varElement.asType())) { + switch (atypeFactory.getCoType(tree)) { + case OwningCollection: + case OwningCollectionWithoutObligation: + checkOwningCollectionField(tree); + // fall through + default: + } + } - // if (!qualHierarchy.isSubtypeShallow(inheritedMCAnno, effectiveMCAnno, tm)) { + return super.visitVariable(tree, p); + } - // checker.reportError( - // tree, - // "inconsistent.mustcall.subtype", - // ElementUtils.getQualifiedName(classEle), - // effectiveMCAnno, - // inheritedMCAnno); - // return false; - // } - // } - // } - // } - // } - // return super.validateType(tree, type); - // } + /** + * Checks validity of an {@code OwningCollection} field {@code field}. Say the type of the + * elements of {@code field} is {@code @MustCall("m"}}. This method checks that the enclosing + * class of {@code field} has a type {@code @MustCall("m2")} for some method {@code m2}, and that + * {@code m2} has an annotation {@code @CollectionFieldDestructor("field")}, guaranteeing that the + * {@code @MustCall} obligation of the field will be satisfied. + * + * @param fieldTree the declaration of the field to check + */ + private void checkOwningCollectionField(VariableTree fieldTree) { + VariableElement fieldElement = TreeUtils.elementFromDeclaration(fieldTree); + List mustCallValues = + atypeFactory.getMustCallValuesOfResourceCollectionComponent(fieldTree); - // TODO maybe return false if user annotation is @OwningCollectionWithoutObligation - // @Override - // public boolean isValidUse( - // AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { - // // MustCallAlias annotations are always permitted on type uses, despite not technically - // // being a part of the type hierarchy. It's necessary to get the annotation from the element - // // because MustCallAlias is aliased to PolyMustCall, which is what useType would contain. - // // Note that isValidUse does not need to consider component types, on which it should be - // // called separately. - // Element elt = TreeUtils.elementFromTree(tree); - // if (elt != null) { - // if (AnnotationUtils.containsSameByClass(elt.getAnnotationMirrors(), MustCallAlias.class)) { - // return true; - // } - // // Need to check the type mirror for ajava-derived annotations and the element itself - // // for human-written annotations from the source code. Getting to the ajava file - // // directly at this point is impossible, so we approximate "the ajava file has an - // // @MustCallAlias annotation" with "there is an @PolyMustCall annotation on the use - // // type, but not in the source code". This only works because none of our inference - // // techniques infer @PolyMustCall, so if @PolyMustCall is present but wasn't in the - // // source, it must have been derived from an @MustCallAlias annotation (which we do - // // infer). - // boolean ajavaFileHasMustCallAlias = - // useType.hasPrimaryAnnotation(PolyMustCall.class) - // && !atypeFactory.containsSameByClass(elt.getAnnotationMirrors(), - // PolyMustCall.class); - // if (ajavaFileHasMustCallAlias) { - // return true; - // } - // } - // return super.isValidUse(declarationType, useType, tree); - // } + if (mustCallValues == null || mustCallValues.isEmpty()) { + return; + } - // TODO this might be important - // @Override - // protected boolean skipReceiverSubtypeCheck( - // MethodInvocationTree tree, - // AnnotatedTypeMirror methodDefinitionReceiver, - // AnnotatedTypeMirror methodCallReceiver) { - // // If you think of the receiver of the method call as an implicit parameter, it has some - // // MustCall type. For example, consider the method call: - // // void foo(@MustCall("bar") ThisClass this) - // // If we now call o.foo() where o has @MustCall({"bar, baz"}), the receiver subtype check - // // would throw an error, since o is not a subtype of @MustCall("bar"). However, since foo - // // cannot take ownership of its receiver, it does not matter what it 'thinks' the @MustCall - // // methods of the receiver are. Hence, it is always sound to skip this check. - // return true; - // } + String error; + RLCCalledMethodsAnnotatedTypeFactory rlAtf = + ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(atypeFactory); + Element enclosingElement = fieldElement.getEnclosingElement(); + List enclosingMustCallValues = rlAtf.getMustCallValues(enclosingElement); + + if (enclosingMustCallValues == null) { + error = + " The enclosing element " + + ElementUtils.getQualifiedName(enclosingElement) + + " doesn't have a @MustCall annotation"; + } else if (enclosingMustCallValues.isEmpty()) { + error = + " The enclosing element " + + ElementUtils.getQualifiedName(enclosingElement) + + " has an empty @MustCall annotation"; + } else { + List siblingsOfOwningField = enclosingElement.getEnclosedElements(); + for (Element siblingElement : siblingsOfOwningField) { + if (siblingElement.getKind() == ElementKind.METHOD + && enclosingMustCallValues.contains(siblingElement.getSimpleName().toString())) { + + ExecutableElement siblingMethod = (ExecutableElement) siblingElement; + + AnnotationMirror collectionFieldDestructorAnno = + atypeFactory.getDeclAnnotation(siblingMethod, CollectionFieldDestructor.class); + if (collectionFieldDestructorAnno != null) { + List destructedFields = + AnnotationUtils.getElementValueArray( + collectionFieldDestructorAnno, + atypeFactory.collectionFieldDestructorValueElement, + String.class); + for (String destructedFieldName : destructedFields) { + if (expressionEqualsField(destructedFieldName, fieldElement)) { + return; + } + } + } + } + } + error = + " No Destructor Method annotated @CollectionFieldDestructor(" + + fieldTree.getName().toString() + + ") found."; + } + checker.reportError( + fieldTree, + "unfulfilled.collection.obligations", + mustCallValues.get(0).equals(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME) + ? "Unknown" + : mustCallValues.get(0), + "field " + fieldTree.getName().toString(), + error); + } - // /** - // * Does not issue any warnings. - // * - // *

This implementation prevents recursing into annotation arguments. Annotation arguments - // are - // * literals, which don't have must-call obligations. - // * - // *

Annotation arguments are treated as return locations for the purposes of defaulting, - // rather - // * than parameter locations. This causes them to default incorrectly when the annotation is - // * defined in bytecode. See https://github.com/typetools/checker-framework/issues/3178 for an - // * explanation of why this is necessary to avoid false positives. - // */ - // @Override - // public Void visitAnnotation(AnnotationTree tree, Void p) { - // return null; - // } + /** + * Determine if the given expression e refers to this.field. + * + * @param e the expression + * @param field the field + * @return true if e refers to this.field + */ + private boolean expressionEqualsField(String e, VariableElement field) { + try { + JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); + return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + // The parsing error will be reported elsewhere, assuming e was derived from an + // annotation. + return false; + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties new file mode 100644 index 000000000000..b962a5445691 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -0,0 +1 @@ +unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index f27afad94eed..5d582b91237d 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -15,9 +15,6 @@ class CollectionOwnershipDefaults { int n = 10; - /* This should default to @OwningCollection */ - Collection resourceCollectionField; - /* * Check that manual MustCall annotations are correctly considered when deciding whether * something is a resoure collection. @@ -43,15 +40,6 @@ public void detectResourceCollection( checkArgIsOwning(l5); } - /* - * Check that resource collection field defaults to @OwningCollection. - */ - void checkResourceCollectionFieldDefault() { - checkArgIsOwning(resourceCollectionField); - // :: error: argument - checkArgIsOCwoO(resourceCollectionField); - } - /* * This method checks that its parameter defaults to @NotOwningCollection. */ From 73af398b534a30ab1e752653af067a668d72f3b1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 17:58:55 +0200 Subject: [PATCH 184/374] demand @CreatesCollectionObligation method invocs on fields to have @CreatesMustCallFor anno on enclosing method --- ...llectionOwnershipAnnotatedTypeFactory.java | 61 +++++++++--- .../CollectionOwnershipStore.java | 2 +- .../CollectionOwnershipTransfer.java | 96 ++++++++----------- .../MustCallConsistencyAnalyzer.java | 77 ++++++++------- 4 files changed, 131 insertions(+), 105 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 1a8e1369018e..f0ba08b43d91 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -1,8 +1,10 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; @@ -39,6 +41,8 @@ import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -375,27 +379,61 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) } /** - * Utility method to get the {@code CollectionOwnershipType} that the given value extracted from a - * store has. + * Utility method to get the flow-sensitive {@code CollectionOwnershipType} that the given node + * has. * - * @param val the store value type - * @return the {@code CollectionOwnershipType} that the given value extracted from a store has + * @param node the node + * @return the {@code CollectionOwnershipType} that the given node has. */ - public CollectionOwnershipType getCoType(CFValue val) { - return val == null ? CollectionOwnershipType.None : getCoType(val.getAnnotations()); + public CollectionOwnershipType getCoType(Node node) { + CollectionOwnershipType res = null; + if (node.getBlock() != null) { + CollectionOwnershipStore coStore = getStoreBefore(node); + try { + JavaExpression jx = JavaExpression.fromNode(node); + CFValue storeVal = coStore.getValue(jx); + res = getCoType(storeVal.getAnnotations()); + } catch (Exception e) { + res = null; + } + } + + if (res == null) { + // not in store + AnnotatedTypeMirror atm = getAnnotatedType(node.getTree()); + return atm == null + ? CollectionOwnershipType.None + : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + } + + return res; } /** - * Utility method to get the {@code CollectionOwnershipType} that the given tree has. + * Utility method to get the flow-sensitive {@code CollectionOwnershipType} that the given tree + * has. * * @param tree the tree * @return the {@code CollectionOwnershipType} that the given tree has. */ public CollectionOwnershipType getCoType(Tree tree) { - AnnotatedTypeMirror atm = getAnnotatedType(tree); - return atm == null - ? CollectionOwnershipType.None - : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + JavaExpression jx = null; + if (tree instanceof ExpressionTree) { + jx = JavaExpression.fromTree((ExpressionTree) tree); + } else if (tree instanceof VariableTree) { + jx = JavaExpression.fromVariableTree((VariableTree) tree); + } + try { + CollectionOwnershipStore coStore = getStoreBefore(tree); + CFValue storeVal = coStore.getValue(jx); + return getCoType(storeVal.getAnnotations()); + } catch (Exception e) { + // No flow-sensitive access. Fall back to annotated type. + AnnotatedTypeMirror atm = getAnnotatedType(tree); + return atm == null + ? CollectionOwnershipType.None + : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + } } /** @@ -410,6 +448,7 @@ public CollectionOwnershipType getCoType(Collection annos) { return CollectionOwnershipType.None; } for (AnnotationMirror anm : annos) { + if (anm == null) continue; if (AnnotationUtils.areSame(anm, NOTOWNINGCOLLECTION)) { return CollectionOwnershipType.NotOwningCollection; } else if (AnnotationUtils.areSame(anm, OWNINGCOLLECTION)) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 383d0a8dafcc..73091c250813 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -39,7 +39,7 @@ protected CFValue newFieldValueAfterMethodCall( CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value); if (superResult == null) { if (atypeFactory.isResourceCollection(fieldAccess.getField().asType())) { - switch (atypeFactory.getCoType(value)) { + switch (atypeFactory.getCoType(value.getAnnotations())) { case OwningCollection: case OwningCollectionWithoutObligation: return value; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 44d424bece49..0f3ddd0f2ea0 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -68,28 +68,23 @@ public TransferResult visitAssignment( Node rhs = node.getExpression(); rhs = getNodeOrTempVar(rhs); JavaExpression rhsJx = JavaExpression.fromNode(rhs); - - CFValue rhsValue = null; - try { - rhsValue = store.getValue(rhsJx); - } catch (Exception e) { - return res; - } - CollectionOwnershipType rhsType = atypeFactory.getCoType(rhsValue); + CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs); // ownership transfer from rhs into lhs usually. // special case desugared assignments of a temporary array variable. - if (rhsType == CollectionOwnershipType.OwningCollection - || rhsType == CollectionOwnershipType.OwningCollectionWithoutObligation) { - if (node.isDesugaredFromEnhancedArrayForLoop()) { - store.clearValue(lhsJx); - store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); - } else { - store.clearValue(rhsJx); - store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); - } + switch (rhsType) { + case OwningCollection: + case OwningCollectionWithoutObligation: + if (node.isDesugaredFromEnhancedArrayForLoop()) { + store.clearValue(lhsJx); + store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } else { + store.clearValue(rhsJx); + store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } + break; + default: } - // boolean assignmentOfOwningCollectionArrayElement = // lhsIsOwningCollection && lhs.getTree().getKind() == Tree.Kind.ARRAY_ACCESS; @@ -130,14 +125,8 @@ private TransferResult transformPotentiallyFu if (loop != null) { CollectionOwnershipStore elseStore = res.getElseStore(); JavaExpression collectionJx = JavaExpression.fromTree(loop.collectionTree); - CFValue collectionValue = null; - try { - collectionValue = elseStore.getValue(collectionJx); - } catch (Exception e) { - return res; - } - CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionValue); + CollectionOwnershipType collectionCoType = atypeFactory.getCoType(loop.collectionTree); if (collectionCoType == CollectionOwnershipType.OwningCollection) { List mustCallValuesOfElements = atypeFactory.getMustCallValuesOfResourceCollectionComponent(loop.collectionTree); @@ -179,13 +168,7 @@ public TransferResult visitMethodInvocation( CollectionOwnershipStore coStore = res.getRegularStore(); Node receiverNode = node.getTarget().getReceiver(); JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); - CFValue receiverCoType = null; - try { - receiverCoType = coStore.getValue(receiverJx); - } catch (Exception e) { - return res; - } - if (atypeFactory.getCoType(receiverCoType) + if (atypeFactory.getCoType(receiverNode) == CollectionOwnershipType.OwningCollectionWithoutObligation) { coStore.clearValue(receiverJx); coStore.insertValue(receiverJx, atypeFactory.OWNINGCOLLECTION); @@ -216,27 +199,31 @@ private TransferResult transferOwnershipForMe Node arg = args.get(i); arg = getNodeOrTempVar(arg); JavaExpression argJx = JavaExpression.fromNode(arg); - CFValue argValue = null; - try { - argValue = store.getValue(argJx); - } catch (Exception e) { - continue; - } - CollectionOwnershipType argType = atypeFactory.getCoType(argValue); + CollectionOwnershipType argType = atypeFactory.getCoType(arg); CollectionOwnershipType paramType = atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); - if (paramType == CollectionOwnershipType.OwningCollection) { - if (argType == CollectionOwnershipType.OwningCollectionWithoutObligation - || argType == CollectionOwnershipType.OwningCollection) { - store.clearValue(argJx); - store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); - } - } else if (paramType == CollectionOwnershipType.OwningCollectionWithoutObligation) { - if (argType == CollectionOwnershipType.OwningCollectionWithoutObligation) { - store.clearValue(argJx); - store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); - } + switch (paramType) { + case OwningCollection: + switch (argType) { + case OwningCollection: + case OwningCollectionWithoutObligation: + store.clearValue(argJx); + store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + break; + default: + } + break; + case OwningCollectionWithoutObligation: + switch (argType) { + case OwningCollectionWithoutObligation: + store.clearValue(argJx); + store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + break; + default: + } + break; + default: } } return new RegularTransferResult( @@ -266,14 +253,7 @@ public TransferResult visitObjectCreation( Node tempVarNode = getNodeOrTempVar(node); JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); - CFValue tempVarVal = null; - try { - tempVarVal = store.getValue(tempVarJx); - } catch (Exception e) { - return result; - } - - CollectionOwnershipType resolvedType = atypeFactory.getCoType(tempVarVal); + CollectionOwnershipType resolvedType = atypeFactory.getCoType(node); if (atypeFactory.isResourceCollection(node.getTree())) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 74a899e33d84..b8a6aa9ffae8 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -772,9 +772,7 @@ public void analyze(ControlFlowGraph cfg) { private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); if (tmpVar != null) { - CollectionOwnershipStore coStore = coAtf.getStoreAfter(node); - CFValue returnCfVal = coStore.getValue(JavaExpression.fromNode(tmpVar)); - CollectionOwnershipType cotype = coAtf.getCoType(returnCfVal); + CollectionOwnershipType cotype = coAtf.getCoType(node); if (cotype == CollectionOwnershipType.OwningCollection) { ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), node.getTree()); @@ -785,7 +783,7 @@ private void addObligationsForOwningCollectionReturn(Set obligations "List of MustCall values of component type is null for OwningCollection return value: " + node); } - if (!ResourceLeakUtils.isIterator(returnCfVal.getUnderlyingType())) { + if (!ResourceLeakUtils.isIterator(node.getType())) { for (String mustCallMethod : mustCallValues) { obligations.add( new CollectionObligation( @@ -810,22 +808,31 @@ private void addObligationsForCreatesCollectionObligationAnno( boolean hasCreatesCollectionObligation = coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; if (hasCreatesCollectionObligation) { - CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); Node receiverNode = node.getTarget().getReceiver(); - JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); - CFValue receiverCoType = null; - try { - receiverCoType = coStore.getValue(receiverJx); - } catch (Exception e) { - return; - } - if (coAtf.getCoType(receiverCoType) - == CollectionOwnershipType.OwningCollectionWithoutObligation) { - List mustCallValues = - coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); - for (String mustCallMethod : mustCallValues) { - obligations.add(CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); - } + Element receiverElt = TreeUtils.elementFromTree(receiverNode.getTree()); + boolean receiverIsField = receiverElt.getKind().isField(); + + switch (coAtf.getCoType(receiverNode)) { + case OwningCollectionWithoutObligation: + if (!receiverIsField) { + List mustCallValues = + coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); + for (String mustCallMethod : mustCallValues) { + obligations.add( + CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); + } + } + // fall through + case OwningCollection: + if (receiverIsField) { + TreePath currentPath = cmAtf.getPath(node.getTree()); + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); + if (enclosingMethodTree != null) { + checkEnclosingMethodIsCreatesMustCallFor(receiverNode, enclosingMethodTree); + } + } + break; + default: } } } @@ -1826,7 +1833,7 @@ private void checkReassignmentToField(Set obligations, AssignmentNod if (!(receiver instanceof LocalVariableNode && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) && !(node.getExpression() instanceof NullLiteralNode)) { - checkEnclosingMethodIsCreatesMustCallFor(node, enclosingMethodTree); + checkEnclosingMethodIsCreatesMustCallFor(node.getTarget(), enclosingMethodTree); } // The following code handles a special case where the field being assigned is itself @@ -1936,23 +1943,24 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) } /** - * Checks that the method that encloses an assignment is marked with @CreatesMustCallFor - * annotation whose target is the object whose field is being re-assigned. + * Checks that the method that encloses an obligation creation for an owning field (e.g. a + * reassignment to an {@code @Owning} field or an invocation of a method annotated + * {@code @CreatesCollectionObligation} on an {@code @OwningCollection} field) is marked + * with @CreatesMustCallFor annotation whose target is the object for whose field an obligation is + * created. * - * @param node an assignment node whose lhs is a non-final, owning field - * @param enclosingMethod the MethodTree in which the re-assignment takes place + * @param node the owning field node + * @param enclosingMethod the MethodTree in which the obligation creation takes place */ - private void checkEnclosingMethodIsCreatesMustCallFor( - AssignmentNode node, MethodTree enclosingMethod) { - Node lhs = node.getTarget(); - if (!(lhs instanceof FieldAccessNode)) { + private void checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree enclosingMethod) { + if (!(node instanceof FieldAccessNode)) { return; } - if (permitStaticOwning && ((FieldAccessNode) lhs).getReceiver() instanceof ClassNameNode) { + if (permitStaticOwning && ((FieldAccessNode) node).getReceiver() instanceof ClassNameNode) { return; } - String receiverString = receiverAsString((FieldAccessNode) lhs); + String receiverString = receiverAsString((FieldAccessNode) node); if ("this".equals(receiverString) && TreeUtils.isConstructor(enclosingMethod)) { // Constructors always create must-call obligations, so there is no need for them to // be annotated. @@ -1970,7 +1978,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor( "missing.creates.mustcall.for", enclosingMethodElt.getSimpleName().toString(), receiverString, - ((FieldAccessNode) lhs).getFieldName()); + ((FieldAccessNode) node).getFieldName()); return; } @@ -1996,7 +2004,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor( "incompatible.creates.mustcall.for", enclosingMethodElt.getSimpleName().toString(), receiverString, - ((FieldAccessNode) lhs).getFieldName(), + ((FieldAccessNode) node).getFieldName(), String.join(", ", checked)); } @@ -2607,12 +2615,11 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { // method. incrementNumMustCall(paramElement); } - CFValue paramCfVal = coStore.getValue(JavaExpression.fromVariableTree(param)); - CollectionOwnershipType cotype = coAtf.getCoType(paramCfVal); + CollectionOwnershipType cotype = coAtf.getCoType(param); if (cotype == CollectionOwnershipType.OwningCollection) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(param); if (mustCallValues != null) { - if (!ResourceLeakUtils.isIterator(paramCfVal.getUnderlyingType())) { + if (!ResourceLeakUtils.isIterator(paramElement.asType())) { for (String mustCallMethod : mustCallValues) { result.add( new CollectionObligation( From 75f3cbf70196e2748f923788f2cb089f91987c04 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 20:58:06 +0200 Subject: [PATCH 185/374] add test for @OC fields --- .../LoopBodyAnalysisTest.java | 8 -- .../OwningCollectionFieldTest.java | 113 ++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 checker/tests/resourceleak-collections/OwningCollectionFieldTest.java diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 8a2a6d5ce28d..10076165c9e6 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -5,14 +5,6 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -@InheritableMustCall({"flush"}) -class Resource implements AutoCloseable { - @Override - public void close() {} - - void flush() {} -} - class LoopBodyAnalysisTests { void fullSatisfyCollection(@OwningCollection Collection resources) { diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java new file mode 100644 index 000000000000..929ed113162e --- /dev/null +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -0,0 +1,113 @@ +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall({"flush", "close"}) +class Resource implements AutoCloseable { + @Override + public void close() {} + + void flush() {} +} + +// 2. check that Aggregator has MustCall method +class Aggregator implements Closeable { + // 1. infer this field as @OwningCollection + List resList = new ArrayList<>(); + + // 5. demand methods adding elements to have @CreatesMustCallFor("this") + @CreatesMustCallFor("this") + void add(@Owning Resource r) { + resList.add(r); + } + + // 3. check that mustcall method has a @CollectionFieldDestructor annotation + // 4. check that field has @OCwO as postcondition + @Override + @CollectionFieldDestructor("resList") + public void close() { + for (Resource r : resList) { + r.close(); + r.flush(); + } + } + + // :: error: missing.creates.mustcall.for + void addIllegal(Resource r) { + resList.add(r); + } + + @CollectionFieldDestructor("resList") + // :: error: contracts.postcondition + public void partialClose() { + for (Resource r : resList) { + r.flush(); + } + } +} + +// has no @MustCall method +class IllegalAggregator { + // :: error: unfulfilled.collection.obligations + List resList = new ArrayList<>(); + + @CollectionFieldDestructor("resList") + public void close() { + for (Resource r : resList) { + r.close(); + r.flush(); + } + } +} + +public class OwningCollectionFieldTest { + void addToAggregator(Resource @OwningCollection [] resources) { + Aggregator agg = new Aggregator(); + for (Resource r : resources) { + agg.add(r); + } + agg.close(); + + // this is not necessary, but the checker would issue a false positive + // without this closing loop. + for (Resource r : resources) { + r.close(); + r.flush(); + } + } + + void failToDestructAggregator(Resource @OwningCollection [] resources) { + // :: error: required.method.not.called + Aggregator agg = new Aggregator(); + for (Resource r : resources) { + agg.add(r); + } + + // this is not necessary, but the checker would issue a false positive + // without this closing loop. + for (Resource r : resources) { + r.close(); + r.flush(); + } + } + + void addAfterDestructing(Resource @OwningCollection [] resources) { + // :: error: required.method.not.called + Aggregator agg = new Aggregator(); + for (Resource r : resources) { + agg.add(r); + } + agg.close(); + agg.add(new Resource()); + + // this is not necessary, but the checker would issue a false positive + // without this closing loop. + for (Resource r : resources) { + r.close(); + r.flush(); + } + } +} From 6134f55863ec327dcdf08ae09c801df29489f1ea Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 25 Jun 2025 21:03:29 +0200 Subject: [PATCH 186/374] make missing.creates.mustcall.for slightly more generic --- .../checkerframework/checker/resourceleak/messages.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index e788043f81f9..6aac9fdb627f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -1,5 +1,5 @@ required.method.not.called=@MustCall %s may not have been invoked on %s or any of its aliases.%nThe type of object is: %s.%nReason for going out of scope: %s -missing.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but does not have a corresponding @CreatesMustCallFor annotation. +missing.creates.mustcall.for=Method %s possibly creates a new obligation for the owning field %s.%s, but does not have a corresponding @CreatesMustCallFor annotation. incompatible.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but its @CreatesMustCallFor annotation targets %s. reset.not.owning=Calling method %s resets the must-call obligations of the expression %s, which is non-owning. Either annotate its declaration with an @Owning annotation, extract it into a local variable, or write a corresponding @CreatesMustCallFor annotation on the method that encloses this statement. creates.mustcall.for.override.invalid=Method %s cannot override method %s, which defines fewer @CreatesMustCallFor targets.%nfound: %s%nrequired: %s From fdf9b0a79a1f6b7a69d9bcc5958657dc9a102521 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:26:06 +0200 Subject: [PATCH 187/374] add method deciding whether rc field is owning or not --- ...llectionOwnershipAnnotatedTypeFactory.java | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f0ba08b43d91..244c70033333 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -2,9 +2,11 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; @@ -55,6 +57,7 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; /** The annotated type factory for the Collection Ownership Checker. */ @@ -248,6 +251,35 @@ public boolean isResourceCollection(TypeMirror t) { return list != null && list.size() > 0; } + /** + * Whether the given tree a resource collection field that is {@code @OwningCollection} by + * declaration, which is the default behavior, i.e. with no different collection ownership + * annotation. + * + * @param tree the tree + * @return true if the tree is a resource collection field that is {@code @OwningCollection} by + * declaration + */ + public boolean isOwningCollectionField(Tree tree) { + if (tree == null) return false; + if (isResourceCollection(tree)) { + Element elt = TreeUtils.elementFromTree(tree); + if (elt != null && elt.getKind().isField()) { + AnnotatedTypeMirror atm = getAnnotatedType(elt); + CollectionOwnershipType fieldType = + getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + switch (fieldType) { + case OwningCollection: + case OwningCollectionWithoutObligation: + return true; + default: + return false; + } + } + } + return false; + } + /** * Returns whether the given AST tree is a resource collection. * @@ -386,26 +418,31 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) * @return the {@code CollectionOwnershipType} that the given node has. */ public CollectionOwnershipType getCoType(Node node) { - CollectionOwnershipType res = null; + CollectionOwnershipType res = CollectionOwnershipType.None; if (node.getBlock() != null) { CollectionOwnershipStore coStore = getStoreBefore(node); try { JavaExpression jx = JavaExpression.fromNode(node); CFValue storeVal = coStore.getValue(jx); - res = getCoType(storeVal.getAnnotations()); + return getCoType(storeVal.getAnnotations()); } catch (Exception e) { - res = null; + res = CollectionOwnershipType.None; } } - if (res == null) { + if (res == CollectionOwnershipType.None) { // not in store - AnnotatedTypeMirror atm = getAnnotatedType(node.getTree()); - return atm == null - ? CollectionOwnershipType.None - : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + if (node.getTree() != null) { + AnnotatedTypeMirror atm = getAnnotatedType(node.getTree()); + if (atm == null) { + Element elt = TreeUtils.elementFromTree(node.getTree()); + atm = getAnnotatedType(elt); + } + if (atm != null) { + return getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + } + } } - return res; } From 0ae2cf5ff05ef7d223d0db618b86fce0a077fcea Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:26:37 +0200 Subject: [PATCH 188/374] introduce owning rc field as @OCwO in constructor --- .../CollectionOwnershipAnnotatedTypeFactory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 244c70033333..867b8d8db8fb 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -571,7 +571,13 @@ protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, b if (isField && isResourceCollection(type.getUnderlyingType())) { AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { - type.replaceAnnotation(OWNINGCOLLECTION); + TreePath currentPath = getPath(tree); + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); + if (enclosingMethodTree != null && TreeUtils.isConstructor(enclosingMethodTree)) { + type.replaceAnnotation(OWNINGCOLLECTIONWITHOUTOBLIGATION); + } else { + type.replaceAnnotation(OWNINGCOLLECTION); + } } } } From dcde6c34b408768038703c6fc464590624312c11 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:27:53 +0200 Subject: [PATCH 189/374] prevent ownership transfer out of owning rc field --- .../CollectionOwnershipTransfer.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 0f3ddd0f2ea0..ac54aed78d94 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -4,6 +4,7 @@ import java.util.HashSet; import java.util.List; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; @@ -39,6 +40,9 @@ public class CollectionOwnershipTransfer /** The type factory. */ private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + /** The checker. */ + private final CollectionOwnershipChecker checker; + /** The MustCall type factory to manage temp vars. */ private final MustCallAnnotatedTypeFactory mcAtf; @@ -52,6 +56,7 @@ public CollectionOwnershipTransfer( super(analysis); atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); + this.checker = checker; } @Override @@ -71,11 +76,13 @@ public TransferResult visitAssignment( CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs); // ownership transfer from rhs into lhs usually. - // special case desugared assignments of a temporary array variable. + // special case desugared assignments of a temporary array variable + // and rhs being owning resource collection field switch (rhsType) { case OwningCollection: case OwningCollectionWithoutObligation: - if (node.isDesugaredFromEnhancedArrayForLoop()) { + if (node.isDesugaredFromEnhancedArrayForLoop() + || atypeFactory.isOwningCollectionField(node.getExpression().getTree())) { store.clearValue(lhsJx); store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); } else { @@ -203,13 +210,14 @@ private TransferResult transferOwnershipForMe CollectionOwnershipType paramType = atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); + Element argElem = TreeUtils.elementFromTree(arg.getTree()); + boolean transferOwnership = false; switch (paramType) { case OwningCollection: switch (argType) { case OwningCollection: case OwningCollectionWithoutObligation: - store.clearValue(argJx); - store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + transferOwnership = true; break; default: } @@ -217,14 +225,22 @@ private TransferResult transferOwnershipForMe case OwningCollectionWithoutObligation: switch (argType) { case OwningCollectionWithoutObligation: - store.clearValue(argJx); - store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + transferOwnership = true; break; default: } break; default: } + if (transferOwnership) { + if (argElem.getKind().isField()) { + checker.reportError( + arg.getTree(), "transfer.owningcollection.field.ownership", arg.getTree().toString()); + } else { + store.clearValue(argJx); + store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + } + } } return new RegularTransferResult( res.getResultValue(), store); From febc5cb30a8df6d1e92edb85cae1a5cb5debb165 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:28:21 +0200 Subject: [PATCH 190/374] fix how owning rc fields are decided in COVisitor --- .../CollectionOwnershipVisitor.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 0e6e8cbee6b5..447998d27dc3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -80,18 +80,9 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { @Override public Void visitVariable(VariableTree tree, Void p) { - VariableElement varElement = TreeUtils.elementFromDeclaration(tree); - - if (varElement.getKind().isField() && atypeFactory.isResourceCollection(varElement.asType())) { - switch (atypeFactory.getCoType(tree)) { - case OwningCollection: - case OwningCollectionWithoutObligation: - checkOwningCollectionField(tree); - // fall through - default: - } + if (atypeFactory.isOwningCollectionField(tree)) { + checkOwningCollectionField(tree); } - return super.visitVariable(tree, p); } From e19a61c00a86f7f4a1c0602e7b36528e38b2ca77 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:30:08 +0200 Subject: [PATCH 191/374] check owning rc field assignments in consistency analyzer --- .../collectionownership/messages.properties | 1 + .../MustCallConsistencyAnalyzer.java | 138 ++++++++++++++++-- .../checker/resourceleak/messages.properties | 1 + 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index b962a5445691..da5750cc646e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1 +1,2 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s +transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index b8a6aa9ffae8..a817806c51f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1306,7 +1306,7 @@ private void removeObligationsAtOwnershipTransferToParameters( */ private void updateObligationsForOwningReturn( Set obligations, ControlFlowGraph cfg, ReturnNode node) { - if (isTransferOwnershipAtReturn(cfg)) { + if (isTransferOwnershipAtReturn(cfg, node)) { Node returnExpr = node.getResult(); returnExpr = getTempVarOrNode(returnExpr); if (returnExpr instanceof LocalVariableNode) { @@ -1342,7 +1342,7 @@ private void updateObligationsForOwningReturn( * @return true iff ownership should be transferred to the return type of the method corresponding * to a CFG */ - private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { + private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg, ReturnNode node) { if (noLightweightOwnership) { // If not using LO, default to always transfer at return, just like Eclipse does. return true; @@ -1358,7 +1358,17 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { coAtf.getCoType(new HashSet<>(executableElement.getReturnType().getAnnotationMirrors())) == CollectionOwnershipType.NotOwningCollection; if (returnTypeHasManualNocAnno) { + // return is @NotOwningCollection. No ownership transfer to call-site. return false; + } else { + // return is @OwningCollection. Report error if @OwningCollection field. + if (coAtf.isOwningCollectionField(node.getResult().getTree())) { + checker.reportError( + node.getTree(), + "transfer.owningcollection.field.ownership", + node.getTree().toString(), + node.getResult().getTree().toString()); + } } return !cmAtf.hasNotOwning(executableElement); } @@ -1388,11 +1398,12 @@ private void updateObligationsForAssignment( // Ownership transfer to @Owning field. if (lhsElement.getKind() == ElementKind.FIELD) { + checkReassignmentToOwningCollectionField(obligations, assignmentNode); boolean isOwningField = !noLightweightOwnership && cmAtf.hasOwning(lhsElement); // Check that the must-call obligations of the lhs have been satisfied, if the field is // non-final and owning. if (isOwningField && cmAtf.canCreateObligations() && !ElementUtils.isFinal(lhsElement)) { - checkReassignmentToField(obligations, assignmentNode); + checkReassignmentToOwningField(obligations, assignmentNode); } // Remove Obligations from local variables, now that the owning field is responsible. @@ -1742,6 +1753,91 @@ private void removeObligationsContainingVar( } } + /** + * Issues an error if the given (re-)assignment to a resource collection field is not valid. A + * (re-)assignment is valid if the field previously is null, declared final, or of type + * {@code @OwningCollectionWithoutObligation}. + * + * @param obligations current tracked Obligations + * @param node an assignment to a non-final, owning field + */ + private void checkReassignmentToOwningCollectionField( + Set obligations, AssignmentNode node) { + Node lhs = node.getTarget(); + Node rhs = node.getExpression(); + + CollectionOwnershipType lhsCoType = coAtf.getCoType(lhs); + CollectionOwnershipType rhsCoType = coAtf.getCoType(rhs); + if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom + || rhsCoType == CollectionOwnershipType.OwningCollectionBottom) { + // not resource collections + return; + } + if (lhsCoType == CollectionOwnershipType.None || rhsCoType == CollectionOwnershipType.None) { + return; + } + + TreePath currentPath = cmAtf.getPath(node.getTree()); + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); + + if (enclosingMethodTree == null) { + // The assignment is taking place in a variable declaration's + // initializer or in an initializer block. + if (node.getTree().getKind() == Tree.Kind.VARIABLE) { + // assignment is a field initializer. Is always permitted. + switch (rhsCoType) { + case OwningCollectionWithoutObligation: + break; + case OwningCollection: + case NotOwningCollection: + throw new BugInCF("rhs of field initializer is " + rhsCoType + ": " + rhs); + default: + } + return; + } else { + // is an initialization block. Not supported. + checker.reportError( + node, + "unfulfilled.collection.obligations", + coAtf.getMustCallValuesOfResourceCollectionComponent(lhs.getTree()).get(0), + lhs.getTree(), + "Field assignment outside method or declaration might overwrite field's current value"); + } + } else { + // The assignment is taking place in a (possibly constructor) method. + switch (lhsCoType) { + case NotOwningCollection: + // doesn't own elements. safe to overwrite. + break; + case OwningCollectionWithoutObligation: + // no obligation. assignment allowed. + // but if rhs is owning, demand CreatesMustCallFor("this") + if (rhsCoType == CollectionOwnershipType.OwningCollection) { + if (checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree)) { + if (coAtf.isOwningCollectionField(lhs.getTree())) { + Set obligationsForVar = + getObligationsForVar(obligations, rhs.getTree()); + for (Obligation obligation : obligationsForVar) { + obligations.remove(obligation); + } + } + } + } + break; + case OwningCollection: + // assignment not allowed + checker.reportError( + node, + "unfulfilled.collection.obligations", + coAtf.getMustCallValuesOfResourceCollectionComponent(lhs.getTree()).get(0), + lhs.getTree(), + "Field assignment might overwrite field's current value"); + break; + default: + } + } + } + /** * Issues an error if the given re-assignment to a non-final, owning field is not valid. A * re-assignment is valid if the called methods type of the lhs before the assignment satisfies @@ -1753,7 +1849,7 @@ private void removeObligationsContainingVar( * @param obligations current tracked Obligations * @param node an assignment to a non-final, owning field */ - private void checkReassignmentToField(Set obligations, AssignmentNode node) { + private void checkReassignmentToOwningField(Set obligations, AssignmentNode node) { Node lhsNode = node.getTarget(); @@ -1951,20 +2047,21 @@ && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) * * @param node the owning field node * @param enclosingMethod the MethodTree in which the obligation creation takes place + * @return true if the check was successful and false if an error had to be reported */ - private void checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree enclosingMethod) { + private boolean checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree enclosingMethod) { if (!(node instanceof FieldAccessNode)) { - return; + return true; } if (permitStaticOwning && ((FieldAccessNode) node).getReceiver() instanceof ClassNameNode) { - return; + return true; } String receiverString = receiverAsString((FieldAccessNode) node); if ("this".equals(receiverString) && TreeUtils.isConstructor(enclosingMethod)) { // Constructors always create must-call obligations, so there is no need for them to // be annotated. - return; + return true; } ExecutableElement enclosingMethodElt = TreeUtils.elementFromDeclaration(enclosingMethod); MustCallAnnotatedTypeFactory mcAtf = cmAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); @@ -1979,7 +2076,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree encl enclosingMethodElt.getSimpleName().toString(), receiverString, ((FieldAccessNode) node).getFieldName()); - return; + return false; } List checked = new ArrayList<>(); @@ -1995,7 +2092,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree encl } if (targetStr.equals(receiverString)) { // This @CreatesMustCallFor annotation matches. - return; + return true; } checked.add(targetStr); } @@ -2006,6 +2103,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor(Node node, MethodTree encl receiverString, ((FieldAccessNode) node).getFieldName(), String.join(", ", checked)); + return false; } /** @@ -2697,6 +2795,26 @@ private static boolean varTrackedInObligations( return null; } + /** + * Gets the set of Obligations whose resource alias set contains the given tree in {@code + * obligations}. + * + * @param obligations a set of Obligations + * @param tree variable tree of interest + * @return the set of Obligations in {@code obligations} whose resource alias set contains {@code + * tree} + */ + /*package-private*/ static Set getObligationsForVar( + Set obligations, Tree tree) { + Set res = new HashSet<>(); + for (Obligation obligation : obligations) { + if (obligation.canBeSatisfiedThrough(tree)) { + res.add(obligation); + } + } + return res; + } + /** * Gets the set of Obligations whose resource alias set contain the given local variable, or an * empty set if none exist. in {@code obligations}. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index 6aac9fdb627f..ec94c7c29f57 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -9,3 +9,4 @@ mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope w mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in pairs (one on a return type and one on a parameter type).%nBut %s required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s +transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. From 246832141550da343203f06f8db4d85e15f588f1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 26 Jun 2025 13:30:15 +0200 Subject: [PATCH 192/374] more field tests --- .../OwningCollectionFieldTest.java | 6 ++ .../OwningCollectionFieldTyping.java | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java index 929ed113162e..a176e3baabc3 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -18,6 +18,12 @@ class Aggregator implements Closeable { // 1. infer this field as @OwningCollection List resList = new ArrayList<>(); + public Aggregator() {} + + public Aggregator(@OwningCollection List list) { + resList = list; + } + // 5. demand methods adding elements to have @CreatesMustCallFor("this") @CreatesMustCallFor("this") void add(@Owning Resource r) { diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java new file mode 100644 index 000000000000..35d02f4e69c8 --- /dev/null +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -0,0 +1,63 @@ +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class ConstructorTakesOwnership implements Closeable { + // :: error: unfulfilled.collection.obligations + List resList = new ArrayList<>(); + @NotOwningCollection List notOwningList = new ArrayList<>(); + + public ConstructorTakesOwnership(@OwningCollection List list) { + resList = list; + } + + // :: error: unfulfilled.collection.obligations + public ConstructorTakesOwnership(@OwningCollection List list, int a) { + if (a > 5) { + // this doesn't remove the obligation of list + notOwningList = list; + } + } + + @Override + public void close() { + for (Resource r : resList) { + r.close(); + r.flush(); + } + } +} + +class OwningCollectionFieldTyping { + // :: error: unfulfilled.collection.obligations + List ocField = new ArrayList<>(); + + void tryTransferringFieldOwnershipAssignment() { + // try to steal ownership + List ownershipStealer = ocField; + // :: error: method.invocation + ownershipStealer.add(new Resource()); + } + + void tryTransferringFieldOwnershipArgumentPassing() { + // try to give away ownership of field to parameter + // :: error: transfer.owningcollection.field.ownership + takeArgumentOwnership(ocField); + } + + void takeArgumentOwnership(@OwningCollection List param) { + // do scary things like adding new elements with calling obligations, or pass + // it on into a class that stores it as an owning field. + param.add(new Resource()); + Aggregator agg = new Aggregator(param); + agg.close(); + } + + List tryTransferringFieldOwnershipReturn() { + // :: error: transfer.owningcollection.field.ownership + return ocField; + } +} From 73b3985f04304b9230484a2340c630824caaf791 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 26 Jun 2025 10:23:48 -0700 Subject: [PATCH 193/374] Javadoc --- .../org/checkerframework/checker/mustcall/MustCallVisitor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index e0dc3b10f29e..afc71fff2c81 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -476,7 +476,8 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { * @param condition the loop condition * @param update the loop update * @return null if any rule is violated, or the name of the array if the loop condition is of the - * form (i < arr.length) or n if it is of the form (i < n), where n is an identifier. + * form {@code i < arr.length} or n if it is of the form {@code i < n}), where n is an + * identifier. */ protected Name verifyAllElementsAreCalledOn( StatementTree init, BinaryTree condition, ExpressionStatementTree update) { From 21f35032498a308e5de665d99d537fdcaf011ab2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 00:49:24 +0200 Subject: [PATCH 194/374] move logic for parsing JavaExp from string from visitor to atf --- ...llectionOwnershipAnnotatedTypeFactory.java | 64 ++++++++++++++++++- .../CollectionOwnershipVisitor.java | 41 ++---------- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 867b8d8db8fb..3bf173b09c1d 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -8,6 +8,7 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -44,6 +45,7 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; @@ -55,6 +57,8 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreePathUtil; @@ -87,7 +91,7 @@ public class CollectionOwnershipAnnotatedTypeFactory public final AnnotationMirror BOTTOM; /** The value element of the {@code @}{@link CollectionFieldDestructor} annotation. */ - public final ExecutableElement collectionFieldDestructorValueElement = + private final ExecutableElement collectionFieldDestructorValueElement = TreeUtils.getMethod(CollectionFieldDestructor.class, "value", 0, processingEnv); /** @@ -496,7 +500,63 @@ public CollectionOwnershipType getCoType(Collection annos) { return CollectionOwnershipType.OwningCollectionBottom; } } - return CollectionOwnershipType.None; + return null; + } + + /** + * Returns the field names in the {@code @CollectionFieldDestructor} annotation that the given + * method has or an empty list if there is no such annotation. + * + * @param method the method + * @return the field names in the {@code @CollectionFieldDestructor} annotation that the given + * method has or an empty list if there is no such annotation. + */ + public List getCollectionFieldDestructorAnnoFields(ExecutableElement method) { + AnnotationMirror collectionFieldDestructorAnno = + getDeclAnnotation(method, CollectionFieldDestructor.class); + if (collectionFieldDestructorAnno != null) { + return AnnotationUtils.getElementValueArray( + collectionFieldDestructorAnno, collectionFieldDestructorValueElement, String.class); + } else { + return new ArrayList(); + } + } + + /** + * Determine if the given expression e refers to this.field. + * + * @param e the expression + * @param field the field + * @return true if e refers to this.field + */ + public boolean expressionEqualsField(String e, VariableElement field) { + try { + JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); + return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + // The parsing error will be reported elsewhere, assuming e was derived from an + // annotation. + return false; + } + } + + /** + * Return a JavaExpression for the given String or null if the conversion fails. + * + * @param s the string + * @param method the method with the annotation + * @return a JavaExpression for the given String or null if the conversion fails + */ + public JavaExpression stringToJavaExpression(String s, ExecutableElement method) { + Tree methodTree = declarationFromElement(method); + if (methodTree != null && (methodTree instanceof MethodTree)) { + try { + return StringToJavaExpression.atMethodBody(s, (MethodTree) methodTree, checker); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + return null; + } + } + return null; } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 447998d27dc3..3bb1021e3737 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -7,19 +7,13 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; -import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.expression.FieldAccess; -import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.util.JavaExpressionParseUtil; -import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationMirrorSet; -import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -128,18 +122,11 @@ private void checkOwningCollectionField(VariableTree fieldTree) { ExecutableElement siblingMethod = (ExecutableElement) siblingElement; - AnnotationMirror collectionFieldDestructorAnno = - atypeFactory.getDeclAnnotation(siblingMethod, CollectionFieldDestructor.class); - if (collectionFieldDestructorAnno != null) { - List destructedFields = - AnnotationUtils.getElementValueArray( - collectionFieldDestructorAnno, - atypeFactory.collectionFieldDestructorValueElement, - String.class); - for (String destructedFieldName : destructedFields) { - if (expressionEqualsField(destructedFieldName, fieldElement)) { - return; - } + List destructedFields = + atypeFactory.getCollectionFieldDestructorAnnoFields(siblingMethod); + for (String destructedFieldName : destructedFields) { + if (atypeFactory.expressionEqualsField(destructedFieldName, fieldElement)) { + return; } } } @@ -158,22 +145,4 @@ private void checkOwningCollectionField(VariableTree fieldTree) { "field " + fieldTree.getName().toString(), error); } - - /** - * Determine if the given expression e refers to this.field. - * - * @param e the expression - * @param field the field - * @return true if e refers to this.field - */ - private boolean expressionEqualsField(String e, VariableElement field) { - try { - JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); - return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); - } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { - // The parsing error will be reported elsewhere, assuming e was derived from an - // annotation. - return false; - } - } } From efa1d3855ccc41cfca6239ea6e400cf0ff61844a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 00:51:28 +0200 Subject: [PATCH 195/374] fix how @OC and owning rc field are decided --- ...llectionOwnershipAnnotatedTypeFactory.java | 85 ++++++++------- .../CollectionOwnershipTransfer.java | 103 ++++++++++++------ .../MustCallConsistencyAnalyzer.java | 86 +++++++++------ 3 files changed, 167 insertions(+), 107 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 3bf173b09c1d..fe8a8c80ad3f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -107,9 +107,7 @@ public enum CollectionOwnershipType { /** the @OwningCollectionWithoutObligation type */ OwningCollectionWithoutObligation, /** the @OwningCollectionBottom type */ - OwningCollectionBottom, - /** if no type in the hierarchy can be determined with certainty */ - None + OwningCollectionBottom }; /** @@ -256,7 +254,7 @@ public boolean isResourceCollection(TypeMirror t) { } /** - * Whether the given tree a resource collection field that is {@code @OwningCollection} by + * Whether the given tree is a resource collection field that is {@code @OwningCollection} by * declaration, which is the default behavior, i.e. with no different collection ownership * annotation. * @@ -272,6 +270,9 @@ public boolean isOwningCollectionField(Tree tree) { AnnotatedTypeMirror atm = getAnnotatedType(elt); CollectionOwnershipType fieldType = getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + if (fieldType == null) { + return false; + } switch (fieldType) { case OwningCollection: case OwningCollectionWithoutObligation: @@ -284,6 +285,36 @@ public boolean isOwningCollectionField(Tree tree) { return false; } + /** + * Whether the given element is a resource collection parameter that is {@code @OwningCollection} + * by declaration, which is the default behavior, i.e. with no different collection ownership + * annotation. + * + * @param elt the element + * @return true if the element is a resource collection parameter that is + * {@code @OwningCollection} by declaration + */ + public boolean isOwningCollectionParameter(Element elt) { + if (elt == null) return false; + if (isResourceCollection(elt.asType())) { + if (elt.getKind() == ElementKind.PARAMETER) { + AnnotatedTypeMirror atm = getAnnotatedType(elt); + CollectionOwnershipType paramType = + getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + if (paramType == null) { + return false; + } + switch (paramType) { + case OwningCollection: + return true; + default: + return false; + } + } + } + return false; + } + /** * Returns whether the given AST tree is a resource collection. * @@ -416,38 +447,20 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) /** * Utility method to get the flow-sensitive {@code CollectionOwnershipType} that the given node - * has. + * has in the given store * * @param node the node - * @return the {@code CollectionOwnershipType} that the given node has. + * @param coStore the store + * @return the {@code CollectionOwnershipType} that the given node has in the given store. */ - public CollectionOwnershipType getCoType(Node node) { - CollectionOwnershipType res = CollectionOwnershipType.None; - if (node.getBlock() != null) { - CollectionOwnershipStore coStore = getStoreBefore(node); - try { - JavaExpression jx = JavaExpression.fromNode(node); - CFValue storeVal = coStore.getValue(jx); - return getCoType(storeVal.getAnnotations()); - } catch (Exception e) { - res = CollectionOwnershipType.None; - } - } - - if (res == CollectionOwnershipType.None) { - // not in store - if (node.getTree() != null) { - AnnotatedTypeMirror atm = getAnnotatedType(node.getTree()); - if (atm == null) { - Element elt = TreeUtils.elementFromTree(node.getTree()); - atm = getAnnotatedType(elt); - } - if (atm != null) { - return getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); - } - } + public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coStore) { + try { + JavaExpression jx = JavaExpression.fromNode(node); + CFValue storeVal = coStore.getValue(jx); + return getCoType(storeVal.getAnnotations()); + } catch (Exception e) { + return null; } - return res; } /** @@ -469,11 +482,7 @@ public CollectionOwnershipType getCoType(Tree tree) { CFValue storeVal = coStore.getValue(jx); return getCoType(storeVal.getAnnotations()); } catch (Exception e) { - // No flow-sensitive access. Fall back to annotated type. - AnnotatedTypeMirror atm = getAnnotatedType(tree); - return atm == null - ? CollectionOwnershipType.None - : getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + return null; } } @@ -486,7 +495,7 @@ public CollectionOwnershipType getCoType(Tree tree) { */ public CollectionOwnershipType getCoType(Collection annos) { if (annos == null) { - return CollectionOwnershipType.None; + return null; } for (AnnotationMirror anm : annos) { if (anm == null) continue; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index ac54aed78d94..b8193152467f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -8,6 +8,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; +import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; @@ -64,33 +65,32 @@ public TransferResult visitAssignment( AssignmentNode node, TransferInput in) { TransferResult res = super.visitAssignment(node, in); - CollectionOwnershipStore store = res.getRegularStore(); - Node lhs = node.getTarget(); lhs = getNodeOrTempVar(lhs); JavaExpression lhsJx = JavaExpression.fromNode(lhs); Node rhs = node.getExpression(); rhs = getNodeOrTempVar(rhs); - JavaExpression rhsJx = JavaExpression.fromNode(rhs); - CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs); + CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); + CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs, coStore); // ownership transfer from rhs into lhs usually. // special case desugared assignments of a temporary array variable // and rhs being owning resource collection field - switch (rhsType) { - case OwningCollection: - case OwningCollectionWithoutObligation: - if (node.isDesugaredFromEnhancedArrayForLoop() - || atypeFactory.isOwningCollectionField(node.getExpression().getTree())) { - store.clearValue(lhsJx); - store.insertValue(lhsJx, atypeFactory.NOTOWNINGCOLLECTION); - } else { - store.clearValue(rhsJx); - store.insertValue(rhsJx, atypeFactory.NOTOWNINGCOLLECTION); - } - break; - default: + if (rhsType != null) { + switch (rhsType) { + case OwningCollection: + case OwningCollectionWithoutObligation: + JavaExpression rhsJx = JavaExpression.fromNode(rhs); + if (node.isDesugaredFromEnhancedArrayForLoop() + || atypeFactory.isOwningCollectionField(node.getExpression().getTree())) { + replaceInStores(res, lhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } else { + replaceInStores(res, rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + } + break; + default: + } } // boolean assignmentOfOwningCollectionArrayElement = // lhsIsOwningCollection && lhs.getTree().getKind() == Tree.Kind.ARRAY_ACCESS; @@ -110,8 +110,7 @@ public TransferResult visitAssignment( // transformWriteToOwningCollection(arrayJx, arrayExpression, node.getExpression(), // store); // } - return new RegularTransferResult( - res.getResultValue(), store); + return res; } /** @@ -164,24 +163,35 @@ public TransferResult visitMethodInvocation( ExecutableElement method = node.getTarget().getMethod(); List args = node.getArguments(); - res = transferOwnershipForMethodInvocation(method, args, res); + res = transferOwnershipForMethodInvocation(method, node, args, res); res = transformPotentiallyFulfillingLoop(res, node.getTree()); // check whether the method is annotated @CreatesCollectionObligation ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); boolean hasCreatesCollectionObligation = atypeFactory.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; + boolean hasCollectionFieldDestructor = + atypeFactory.getDeclAnnotation(methodElement, CollectionFieldDestructor.class) != null; if (hasCreatesCollectionObligation) { - CollectionOwnershipStore coStore = res.getRegularStore(); Node receiverNode = node.getTarget().getReceiver(); JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); - if (atypeFactory.getCoType(receiverNode) + CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); + if (atypeFactory.getCoType(receiverNode, coStore) == CollectionOwnershipType.OwningCollectionWithoutObligation) { - coStore.clearValue(receiverJx); - coStore.insertValue(receiverJx, atypeFactory.OWNINGCOLLECTION); + replaceInStores(res, receiverJx, atypeFactory.OWNINGCOLLECTION); + } + } + if (hasCollectionFieldDestructor) { + List destructedFields = + atypeFactory.getCollectionFieldDestructorAnnoFields(methodElement); + for (String destructedFieldName : destructedFields) { + JavaExpression fieldExpr = + atypeFactory.stringToJavaExpression(destructedFieldName, methodElement); + if (fieldExpr != null) { + replaceInStores(res, fieldExpr, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); + } } } - return res; } @@ -190,25 +200,30 @@ public TransferResult visitMethodInvocation( * parameter lose ownership. * * @param method the method whose parameters are checked + * @param node the node of the invocation * @param args the list of method arguments * @param res the transfer result so far * @return the updated transfer result */ private TransferResult transferOwnershipForMethodInvocation( ExecutableElement method, + Node node, List args, TransferResult res) { List params = method.getParameters(); - CollectionOwnershipStore store = res.getRegularStore(); for (int i = 0; i < Math.min(args.size(), params.size()); i++) { VariableElement param = params.get(i); Node arg = args.get(i); arg = getNodeOrTempVar(arg); JavaExpression argJx = JavaExpression.fromNode(arg); - CollectionOwnershipType argType = atypeFactory.getCoType(arg); + CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); + CollectionOwnershipType argType = atypeFactory.getCoType(arg, coStore); CollectionOwnershipType paramType = atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); + if (paramType == null) { + continue; + } Element argElem = TreeUtils.elementFromTree(arg.getTree()); boolean transferOwnership = false; @@ -237,13 +252,11 @@ private TransferResult transferOwnershipForMe checker.reportError( arg.getTree(), "transfer.owningcollection.field.ownership", arg.getTree().toString()); } else { - store.clearValue(argJx); - store.insertValue(argJx, atypeFactory.NOTOWNINGCOLLECTION); + replaceInStores(res, argJx, atypeFactory.NOTOWNINGCOLLECTION); } } } - return new RegularTransferResult( - res.getResultValue(), store); + return res; } @Override @@ -255,7 +268,7 @@ public TransferResult visitObjectCreation( ExecutableElement constructor = TreeUtils.elementFromUse(node.getTree()); List args = node.getArguments(); - result = transferOwnershipForMethodInvocation(constructor, args, result); + result = transferOwnershipForMethodInvocation(constructor, node, args, result); // the return value defaulting logic cannot recognize that a diamond constructed collection // is a resource collection, as it runs before the type variable is inferred: @@ -269,7 +282,8 @@ public TransferResult visitObjectCreation( Node tempVarNode = getNodeOrTempVar(node); JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); - CollectionOwnershipType resolvedType = atypeFactory.getCoType(node); + CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); + CollectionOwnershipType resolvedType = atypeFactory.getCoType(tempVarNode, coStore); if (atypeFactory.isResourceCollection(node.getTree())) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { @@ -326,6 +340,29 @@ protected static void insertInStores( } } + /** + * Inserts newAnno as the value into all stores (conditional or not) in the result for node, + * clearing the previous value first. + * + * @param result the TransferResult holding the stores to modify + * @param target the receiver whose value should be modified + * @param newAnno the new value + */ + protected static void replaceInStores( + TransferResult result, + JavaExpression target, + AnnotationMirror newAnno) { + if (result.containsTwoStores()) { + result.getThenStore().clearValue(target); + result.getElseStore().clearValue(target); + result.getThenStore().insertValue(target, newAnno); + result.getElseStore().insertValue(target, newAnno); + } else { + result.getRegularStore().clearValue(target); + result.getRegularStore().insertValue(target, newAnno); + } + } + /** * Removes casts from {@code node} and returns the temp-var corresponding to it if it exists or * else {@code node} with removed casts. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index a817806c51f9..6091467e21fa 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -772,7 +772,8 @@ public void analyze(ControlFlowGraph cfg) { private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); if (tmpVar != null) { - CollectionOwnershipType cotype = coAtf.getCoType(node); + CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipType cotype = coAtf.getCoType(node, coStore); if (cotype == CollectionOwnershipType.OwningCollection) { ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), node.getTree()); @@ -809,12 +810,16 @@ private void addObligationsForCreatesCollectionObligationAnno( coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; if (hasCreatesCollectionObligation) { Node receiverNode = node.getTarget().getReceiver(); - Element receiverElt = TreeUtils.elementFromTree(receiverNode.getTree()); - boolean receiverIsField = receiverElt.getKind().isField(); + boolean receiverIsOwningField = coAtf.isOwningCollectionField(receiverNode.getTree()); - switch (coAtf.getCoType(receiverNode)) { + CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipType receiverType = coAtf.getCoType(receiverNode, coStore); + if (receiverType == null) { + throw new BugInCF("Method receiver not in collection ownership store: " + receiverNode); + } + switch (receiverType) { case OwningCollectionWithoutObligation: - if (!receiverIsField) { + if (!receiverIsOwningField) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); for (String mustCallMethod : mustCallValues) { @@ -824,7 +829,7 @@ private void addObligationsForCreatesCollectionObligationAnno( } // fall through case OwningCollection: - if (receiverIsField) { + if (receiverIsOwningField) { TreePath currentPath = cmAtf.getPath(node.getTree()); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); if (enclosingMethodTree != null) { @@ -1280,10 +1285,8 @@ private void removeObligationsAtOwnershipTransferToParameters( obligations.remove(localObligation); } } - boolean paramHasManualOcAnno = - coAtf.getCoType(new HashSet<>(parameter.asType().getAnnotationMirrors())) - == CollectionOwnershipType.OwningCollection; - if (paramHasManualOcAnno) { + + if (coAtf.isOwningCollectionParameter(parameter)) { Set obligationsForVar = getObligationsForVar(obligations, local); for (Obligation obligation : obligationsForVar) { obligations.remove(obligation); @@ -1766,14 +1769,7 @@ private void checkReassignmentToOwningCollectionField( Node lhs = node.getTarget(); Node rhs = node.getExpression(); - CollectionOwnershipType lhsCoType = coAtf.getCoType(lhs); - CollectionOwnershipType rhsCoType = coAtf.getCoType(rhs); - if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom - || rhsCoType == CollectionOwnershipType.OwningCollectionBottom) { - // not resource collections - return; - } - if (lhsCoType == CollectionOwnershipType.None || rhsCoType == CollectionOwnershipType.None) { + if (!coAtf.isResourceCollection(lhs.getType()) && !coAtf.isResourceCollection(rhs.getType())) { return; } @@ -1785,14 +1781,14 @@ private void checkReassignmentToOwningCollectionField( // initializer or in an initializer block. if (node.getTree().getKind() == Tree.Kind.VARIABLE) { // assignment is a field initializer. Is always permitted. - switch (rhsCoType) { - case OwningCollectionWithoutObligation: - break; - case OwningCollection: - case NotOwningCollection: - throw new BugInCF("rhs of field initializer is " + rhsCoType + ": " + rhs); - default: - } + // switch (rhsCoType) { + // case OwningCollectionWithoutObligation: + // break; + // case OwningCollection: + // case NotOwningCollection: + // throw new BugInCF("rhs of field initializer is " + rhsCoType + ": " + rhs); + // default: + // } return; } else { // is an initialization block. Not supported. @@ -1805,6 +1801,25 @@ private void checkReassignmentToOwningCollectionField( } } else { // The assignment is taking place in a (possibly constructor) method. + CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipType lhsCoType = coAtf.getCoType(getTempVarOrNode(lhs), coStore); + if (lhsCoType == null) { + throw new BugInCF( + "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); + } + CollectionOwnershipType rhsCoType = coAtf.getCoType(getTempVarOrNode(rhs), coStore); + if (rhsCoType == null) { + throw new BugInCF( + "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); + } + if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom + || rhsCoType == CollectionOwnershipType.OwningCollectionBottom) { + throw new BugInCF( + "Expression " + + node + + " has resource collection operand, but @OwningCollectionBottom type."); + } + switch (lhsCoType) { case NotOwningCollection: // doesn't own elements. safe to overwrite. @@ -1813,13 +1828,11 @@ private void checkReassignmentToOwningCollectionField( // no obligation. assignment allowed. // but if rhs is owning, demand CreatesMustCallFor("this") if (rhsCoType == CollectionOwnershipType.OwningCollection) { - if (checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree)) { - if (coAtf.isOwningCollectionField(lhs.getTree())) { - Set obligationsForVar = - getObligationsForVar(obligations, rhs.getTree()); - for (Obligation obligation : obligationsForVar) { - obligations.remove(obligation); - } + checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree); + if (coAtf.isOwningCollectionField(lhs.getTree())) { + Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); + for (Obligation obligation : obligationsForVar) { + obligations.remove(obligation); } } } @@ -1827,7 +1840,7 @@ private void checkReassignmentToOwningCollectionField( case OwningCollection: // assignment not allowed checker.reportError( - node, + node.getTree(), "unfulfilled.collection.obligations", coAtf.getMustCallValuesOfResourceCollectionComponent(lhs.getTree()).get(0), lhs.getTree(), @@ -2713,9 +2726,10 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { // method. incrementNumMustCall(paramElement); } - CollectionOwnershipType cotype = coAtf.getCoType(param); - if (cotype == CollectionOwnershipType.OwningCollection) { + if (coAtf.isOwningCollectionParameter(paramElement)) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(param); + { + } if (mustCallValues != null) { if (!ResourceLeakUtils.isIterator(paramElement.asType())) { for (String mustCallMethod : mustCallValues) { From bf14a97f3b02077c0d5fed87b3125e5426f3bd0c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 00:51:48 +0200 Subject: [PATCH 196/374] check field accesses to prevent accessing foreign owning rc fields --- .../MustCallConsistencyAnalyzer.java | 29 ++++++++++++++++++- .../checker/resourceleak/messages.properties | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 6091467e21fa..f3bd2a3fc386 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -842,6 +842,26 @@ private void addObligationsForCreatesCollectionObligationAnno( } } + /** + * Prevents leaking of resource collection fields owned by the enclosing class by simply + * forbidding any access to it from anyone outside the immediate class. + * + * @param node the field access to check + */ + private void checkOwningResourceCollectionFieldAccess(FieldAccessNode node) { + if (coAtf.isOwningCollectionField(node.getTree())) { + String receiverString = receiverAsString(node); + boolean isSelfAccess = "this".equals(receiverString); + boolean isSuperAccess = "super".equals(receiverString); + if (!isSelfAccess && !isSuperAccess) { + checker.reportError( + node.getTree(), + "foreign.owningcollection.field.access", + node.getFieldName().toString()); + } + } + } + /** * Update a set of Obligations to account for a method or constructor invocation. * @@ -2139,6 +2159,11 @@ private String receiverAsString(FieldAccessNode fieldAccessNode) { if (receiver instanceof SuperNode) { return "super"; } + if (receiver instanceof FieldAccessNode) { + return receiverAsString((FieldAccessNode) receiver) + + "." + + ((FieldAccessNode) receiver).getFieldName(); + } throw new TypeSystemError( "unexpected receiver of field assignment: " + receiver + " of type " + receiver.getClass()); } @@ -2369,6 +2394,8 @@ private void propagateObligationsToSuccessorBlocks( updateObligationsForOwningReturn(obligations, cfg, (ReturnNode) node); } else if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { updateObligationsForInvocation(obligations, node, successorAndExceptionType.second); + } else if (node instanceof FieldAccessNode) { + checkOwningResourceCollectionFieldAccess((FieldAccessNode) node); } // All other types of nodes are ignored. This is safe, because other kinds of // nodes cannot create or modify the resource-alias sets that the algorithm is @@ -2742,7 +2769,7 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { paramElement, param, hasMustCallAlias)), - Collections.singleton(MethodExitKind.NORMAL_RETURN))); + MethodExitKind.ALL)); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index ec94c7c29f57..46103530179d 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -10,3 +10,4 @@ mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. +foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field owned by a different class. From 7fd44cceb8d7ba524546c1eec36d8a509280e04a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 00:52:08 +0200 Subject: [PATCH 197/374] new field tests --- .../LoopBodyAnalysisTest.java | 7 +- .../OwningCollectionFieldTest.java | 23 +++++++ .../OwningCollectionFieldTyping.java | 66 ++++++++++++++++++- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 10076165c9e6..566ce8c72daf 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -111,8 +111,11 @@ void earlyBreak(Resource @OwningCollection [] resources) { void tryWithResources(Resource @OwningCollection [] resources) { for (Resource r : resources) { - try (Resource auto = r) { - auto.flush(); + try { + try (Resource auto = r) { + auto.flush(); + } + } catch (Exception e) { } } } diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java index a176e3baabc3..1c3ad4417e05 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -85,6 +85,29 @@ void addToAggregator(Resource @OwningCollection [] resources) { } } + // Mainly test that accessing the owning resource collection field of another class is + // forbidden. + // since an obligaton for the field agg.resList is created, a @CreatesMustCallFor("agg") + // annotation is expected, which is nonsensical (why the assignment of this field itself + // is forbidden). + // :: error: missing.creates.mustcall.for + void accessOwningFieldAfterClosing(Resource @OwningCollection [] resources) { + Aggregator agg = new Aggregator(); + for (Resource r : resources) { + agg.add(r); + } + agg.close(); + // :: error: foreign.owningcollection.field.access + agg.resList.add(new Resource()); + + // this is not necessary, but the checker would issue a false positive + // without this closing loop. + for (Resource r : resources) { + r.close(); + r.flush(); + } + } + void failToDestructAggregator(Resource @OwningCollection [] resources) { // :: error: required.method.not.called Aggregator agg = new Aggregator(); diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 35d02f4e69c8..8a69fc2559f1 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -6,9 +6,8 @@ import org.checkerframework.checker.mustcall.qual.*; class ConstructorTakesOwnership implements Closeable { - // :: error: unfulfilled.collection.obligations List resList = new ArrayList<>(); - @NotOwningCollection List notOwningList = new ArrayList<>(); + @NotOwningCollection List notOwningField = new ArrayList<>(); public ConstructorTakesOwnership(@OwningCollection List list) { resList = list; @@ -18,10 +17,71 @@ public ConstructorTakesOwnership(@OwningCollection List list) { public ConstructorTakesOwnership(@OwningCollection List list, int a) { if (a > 5) { // this doesn't remove the obligation of list - notOwningList = list; + notOwningField = list; + } + } + + public ConstructorTakesOwnership(@OwningCollection List list, float a) { + resList = list; + if (a > 5) { + // illegal reassignment + // :: error: unfulfilled.collection.obligations + resList = new ArrayList<>(); + } + } + + // no justification for reassignment. Not allowed. + public void reassignCollectionFieldIllegal() { + // :: error: unfulfilled.collection.obligations + resList = new ArrayList<>(); + } + + // allowed since resList has @OwningCollectionWithoutObligation at reassignment time. + // No CreatesMustCallFor("this") required, since the rhs is @OwningCollectionWithoutObligation. + public void reassignCollectionFieldLegal() { + for (Resource r : resList) { + r.close(); + r.flush(); } + resList = new ArrayList<>(); + } + + // assignment allowed since resList has @OwningCollectionWithoutObligation at reassignment time, + // but method + // is missing an @CreatesMustCallFor("this") + // :: error: missing.creates.mustcall.for + public void reassignCollectionFieldMissingCMCF() { + for (Resource r : resList) { + r.close(); + r.flush(); + } + List newList = new ArrayList<>(); + newList.add(new Resource()); + resList = newList; + } + + // allowed since resList has @OwningCollectionWithoutObligation at reassignment time + public void reassignCollectionFieldAfterDestructor() { + close(); + resList = new ArrayList<>(); + } + + // allowed since assignment cannot overwrite anything + public void reassignCollectionFieldIfNull() { + if (resList != null) { + resList = new ArrayList<>(); + } + } + + // assignment allowed since field not owned by class. + // treated as normal variable. + public void reassignCollectionNotOwningField() { + notOwningField = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + notOwningField.add(new Resource()); } + @CollectionFieldDestructor("resList") @Override public void close() { for (Resource r : resList) { From f3442c3888ee43f17b07ba811d0424d082e62a22 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 00:55:08 +0200 Subject: [PATCH 198/374] comment field null test, not implemented yet --- .../OwningCollectionFieldTyping.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 8a69fc2559f1..c7a8f2cff4c6 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -66,12 +66,13 @@ public void reassignCollectionFieldAfterDestructor() { resList = new ArrayList<>(); } - // allowed since assignment cannot overwrite anything - public void reassignCollectionFieldIfNull() { - if (resList != null) { - resList = new ArrayList<>(); - } - } + // TODO SCK: uncomment this test + // // allowed since assignment cannot overwrite anything + // public void reassignCollectionFieldIfNull() { + // if (resList != null) { + // resList = new ArrayList<>(); + // } + // } // assignment allowed since field not owned by class. // treated as normal variable. From 5474b792bad9e5e21836b694d190cbc1cc6cfdae Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 01:23:48 +0200 Subject: [PATCH 199/374] add missing doc --- .../CollectionOwnershipStore.java | 16 ++++++++++++++-- .../MustCallConsistencyAnalyzer.java | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 73091c250813..0d5e988b2822 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -8,19 +8,31 @@ /** * The CollectionOwnership Store behaves like CFAbstractStore but keeps @OwningCollection fields in * the store. This is justified by the strict access rules of such fields. Keeping the field in the - * store is required for verifying the postcondition annotation {@link CollectionFieldDestructor}. + * store is required for verifying the postcondition annotation {@code @CollectionFieldDestructor}. */ public class CollectionOwnershipStore extends CFAbstractStore { + /** the annotated type factory */ private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + /** + * Constructs a collection ownership store. + * + * @param analysis the collection ownership analysis + * @param sequentialSemantics whether to use sequential semantics + */ public CollectionOwnershipStore( CollectionOwnershipAnalysis analysis, boolean sequentialSemantics) { super(analysis, sequentialSemantics); this.atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); } - /** Copy constructor. */ + /** + * Copy constructor. + * + * @param analysis the collection ownership analysis + * @param other the store to construct from + */ public CollectionOwnershipStore( CollectionOwnershipAnalysis analysis, CFAbstractStore other) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index a2cdfac77a0d..2dd529d768e6 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1362,6 +1362,7 @@ private void updateObligationsForOwningReturn( * Returns true when there is no {@link NotOwning} annotation on the return type. * * @param cfg the CFG of the method + * @param node the return node * @return true iff ownership should be transferred to the return type of the method corresponding * to a CFG */ From 47004435df4fa85228997205b2d7d5344c3501bf Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 02:03:28 +0200 Subject: [PATCH 200/374] please new preferences of formatter --- ...llectionOwnershipAnnotatedTypeFactory.java | 15 +++---- .../CollectionOwnershipStore.java | 2 +- .../MustCallAnnotatedTypeFactory.java | 44 +++++++++---------- .../checker/mustcall/MustCallVisitor.java | 17 ++++--- .../MustCallConsistencyAnalyzer.java | 6 +-- .../checkerframework/javacutil/TreeUtils.java | 4 +- 6 files changed, 43 insertions(+), 45 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index fe8a8c80ad3f..f2b052dcf2b1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -254,19 +254,18 @@ public boolean isResourceCollection(TypeMirror t) { } /** - * Whether the given tree is a resource collection field that is {@code @OwningCollection} by + * Whether the given element is a resource collection field that is {@code @OwningCollection} by * declaration, which is the default behavior, i.e. with no different collection ownership * annotation. * - * @param tree the tree - * @return true if the tree is a resource collection field that is {@code @OwningCollection} by + * @param elt the element + * @return true if the element is a resource collection field that is {@code @OwningCollection} by * declaration */ - public boolean isOwningCollectionField(Tree tree) { - if (tree == null) return false; - if (isResourceCollection(tree)) { - Element elt = TreeUtils.elementFromTree(tree); - if (elt != null && elt.getKind().isField()) { + public boolean isOwningCollectionField(Element elt) { + if (elt == null) return false; + if (isResourceCollection(elt.asType())) { + if (elt.getKind().isField()) { AnnotatedTypeMirror atm = getAnnotatedType(elt); CollectionOwnershipType fieldType = getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 0d5e988b2822..5bca0b8a2012 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -41,7 +41,7 @@ public CollectionOwnershipStore( } /* - * Keep {@code OwningCollection} fields in the store. + * Keep OwningCollection fields in the store. */ @Override protected CFValue newFieldValueAfterMethodCall( diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 1d27dd377c14..7ce8514ffd25 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -222,24 +222,24 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar } } - /* - * Changes the type parameter annotations of collections and iterators to @MustCall if they are currently - * @MustCallUnknown. + /** + * Changes the type parameter annotations of collections and iterators to {@code @MustCall} if + * they are currently {@code @MustCallUnknown}. * *

This is necessary, as the type variable upper bounds for collections is * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and * imprecise. * - *

This method changes the type parameter annotations for declared types directly. The other overload - * with access to {@code Element}s handles type parameter annotations for method return types and parameters, - * such that the changes are 'visible' at call-site as well as within the method. Changing this on the - * {@code Tree} is not sufficient. - * The reason that declared types are handled here is that for object initializations where the type - * parameter is left for inference, we don't want to change the type parameter annotation here, but wait - * for the inference instead, which instantiates it with the inferred type and corresponding annotation. - * {@code new Object<>()} - * Access to the {@code Tree} allows us to detect whether we have a new class tree without type parameters. + *

This method changes the type parameter annotations for declared types directly. The other + * overload with access to {@code Element}s handles type parameter annotations for method return + * types and parameters, such that the changes are 'visible' at call-site as well as within the + * method. Changing this on the {@code Tree} is not sufficient. The reason that declared types are + * handled here is that for object initializations where the type parameter is left for inference, + * we don't want to change the type parameter annotation here, but wait for the inference instead, + * which instantiates it with the inferred type and corresponding annotation. {@code new + * Object<>()} Access to the {@code Tree} allows us to detect whether we have a new class tree + * without type parameters. */ @Override public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { @@ -252,22 +252,22 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool } } - /* - * Changes the type parameter annotations of collections and iterators to @MustCall if they are currently - * @MustCallUnknown. + /** + * Changes the type parameter annotations of collections and iterators to {@code @MustCall} if + * they are currently {@code @MustCallUnknown}. * *

This is necessary, as the type variable upper bounds for collections is * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and * imprecise. * - *

This method changes the type parameter annotations of {@code Element}s, which is the preferred - * way for method return types and parameters, such that the changes are 'visible' at call-site as well - * as within the method. The type parameter annotations at other places is handled in the overload of this - * method with access to the {@code Tree}. The reason is that for object initializations where the type - * parameter is left for inference, we don't want to change the type parameter annotation here, but wait - * for the inference instead, which instantiates it with the inferred type and corresponding annotation. - * {@code new Object<>()} + *

This method changes the type parameter annotations of {@code Element}s, which is the + * preferred way for method return types and parameters, such that the changes are 'visible' at + * call-site as well as within the method. The type parameter annotations at other places is + * handled in the overload of this method with access to the {@code Tree}. The reason is that for + * object initializations where the type parameter is left for inference, we don't want to change + * the type parameter annotation here, but wait for the inference instead, which instantiates it + * with the inferred type and corresponding annotation. {@code new Object<>()} */ @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index b776ac914468..7f2c539ba54f 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -507,14 +507,14 @@ protected Name verifyAllElementsAreCalledOn( && TreeUtils.isSizeAccess(condition.getRightOperand())) { ExpressionTree methodSelect = ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); - if (methodSelect.getKind() == Tree.Kind.MEMBER_SELECT) { + if (methodSelect instanceof MemberSelectTree) { MemberSelectTree mst = (MemberSelectTree) methodSelect; Element elt = TreeUtils.elementFromTree(mst.getExpression()); if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { return getNameFromExpressionTree(mst.getExpression()); } } - } else if (condition.getRightOperand().getKind() == Tree.Kind.IDENTIFIER) { + } else if (condition.getRightOperand() instanceof IdentifierTree) { return getNameFromExpressionTree(condition.getRightOperand()); } } @@ -725,16 +725,15 @@ private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collect */ private boolean isIthCollectionElement(Tree tree, Name index) { if (tree == null || index == null) return false; - if (tree.getKind() == Tree.Kind.METHOD_INVOCATION + if (tree instanceof MethodInvocationTree && index == getNameFromExpressionTree(TreeUtils.getIdxForGetCall(tree))) { MethodInvocationTree mit = (MethodInvocationTree) tree; ExpressionTree methodSelect = mit.getMethodSelect(); - assert methodSelect.getKind() == Tree.Kind.MEMBER_SELECT - : "method selection of object.get() expected to be memberSelectTree, but is " - + methodSelect.getKind(); - MemberSelectTree mst = (MemberSelectTree) methodSelect; - Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); - return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + if (methodSelect instanceof MemberSelectTree) { + MemberSelectTree mst = (MemberSelectTree) methodSelect; + Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); + return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + } } return false; } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 2dd529d768e6..9fed5a3fe5c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1800,7 +1800,7 @@ private void checkReassignmentToOwningCollectionField( if (enclosingMethodTree == null) { // The assignment is taking place in a variable declaration's // initializer or in an initializer block. - if (node.getTree().getKind() == Tree.Kind.VARIABLE) { + if (node.getTree() instanceof VariableTree) { // assignment is a field initializer. Is always permitted. // switch (rhsCoType) { // case OwningCollectionWithoutObligation: @@ -3279,7 +3279,7 @@ private void patternMatchEnhancedCollectionForLoop( } node = nodeIterator.next(); isAssignmentOfIterVar = false; - if ((node instanceof AssignmentNode) && node.getTree().getKind() == Tree.Kind.VARIABLE) { + if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { loopVarNode = ((AssignmentNode) node).getTarget(); VariableTree iterVarDecl = (VariableTree) node.getTree(); isAssignmentOfIterVar = iterVarDecl.getName() == loopVariable.getName(); @@ -3488,7 +3488,7 @@ public void analyzeObligationFulfillingLoop( // Add an obligation for the element of the collection iterated over Obligation collectionElementObligation = Obligation.fromTree(collectionElement); - if (collectionElement.getKind() == Tree.Kind.VARIABLE) { + if (collectionElement instanceof VariableTree) { VariableElement varElt = TreeUtils.elementFromDeclaration((VariableTree) collectionElement); boolean hasMustCallAlias = cmAtf.hasMustCallAlias(varElt); collectionElementObligation = diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 57aead933ef1..88e95ebfcb26 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1732,7 +1732,7 @@ && getFieldName(tree).equals("length")) { * @return ExpressionTree of {@code idx} if tree is {@code Collection.get(idx)} and null else */ public static @Nullable ExpressionTree getIdxForGetCall(Tree tree) { - if (tree.getKind() == Tree.Kind.METHOD_INVOCATION + if ((tree instanceof MethodInvocationTree) && isNamedMethodCall("get", (MethodInvocationTree) tree)) { return ((MethodInvocationTree) tree).getArguments().get(0); } @@ -1746,7 +1746,7 @@ && isNamedMethodCall("get", (MethodInvocationTree) tree)) { * @return whether the given tree is of the form object.size() */ public static boolean isSizeAccess(Tree tree) { - return tree.getKind() == Tree.Kind.METHOD_INVOCATION + return (tree instanceof MethodInvocationTree) && isNamedMethodCall("size", (MethodInvocationTree) tree); } From 410f092a550eca10678abe33de56198a4b352ba8 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 02:04:27 +0200 Subject: [PATCH 201/374] use new isOwningCollectionField in costore as well --- .../collectionownership/CollectionOwnershipStore.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 5bca0b8a2012..0d5846b38b0b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -50,14 +50,8 @@ protected CFValue newFieldValueAfterMethodCall( CFValue value) { CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value); if (superResult == null) { - if (atypeFactory.isResourceCollection(fieldAccess.getField().asType())) { - switch (atypeFactory.getCoType(value.getAnnotations())) { - case OwningCollection: - case OwningCollectionWithoutObligation: - return value; - default: - return superResult; - } + if (atypeFactory.isOwningCollectionField(fieldAccess.getField())) { + return value; } } return superResult; From 77d69469e8fddfac0bd05c7c442e274c67f5e5db Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 02:05:15 +0200 Subject: [PATCH 202/374] isocfield now expects element instead of tree --- .../CollectionOwnershipTransfer.java | 3 ++- .../CollectionOwnershipVisitor.java | 2 +- .../resourceleak/MustCallConsistencyAnalyzer.java | 10 ++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index b8193152467f..83029ef3c644 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -83,7 +83,8 @@ public TransferResult visitAssignment( case OwningCollectionWithoutObligation: JavaExpression rhsJx = JavaExpression.fromNode(rhs); if (node.isDesugaredFromEnhancedArrayForLoop() - || atypeFactory.isOwningCollectionField(node.getExpression().getTree())) { + || atypeFactory.isOwningCollectionField( + TreeUtils.elementFromTree(node.getExpression().getTree()))) { replaceInStores(res, lhsJx, atypeFactory.NOTOWNINGCOLLECTION); } else { replaceInStores(res, rhsJx, atypeFactory.NOTOWNINGCOLLECTION); diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 3bb1021e3737..6116c931f2c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -74,7 +74,7 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { @Override public Void visitVariable(VariableTree tree, Void p) { - if (atypeFactory.isOwningCollectionField(tree)) { + if (atypeFactory.isOwningCollectionField(TreeUtils.elementFromDeclaration(tree))) { checkOwningCollectionField(tree); } return super.visitVariable(tree, p); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 9fed5a3fe5c1..ba81fb304fbb 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -810,7 +810,9 @@ private void addObligationsForCreatesCollectionObligationAnno( coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; if (hasCreatesCollectionObligation) { Node receiverNode = node.getTarget().getReceiver(); - boolean receiverIsOwningField = coAtf.isOwningCollectionField(receiverNode.getTree()); + receiverNode = removeCastsAndGetTmpVarIfPresent(receiverNode); + boolean receiverIsOwningField = + coAtf.isOwningCollectionField(TreeUtils.elementFromTree(receiverNode.getTree())); CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); CollectionOwnershipType receiverType = coAtf.getCoType(receiverNode, coStore); @@ -849,7 +851,7 @@ private void addObligationsForCreatesCollectionObligationAnno( * @param node the field access to check */ private void checkOwningResourceCollectionFieldAccess(FieldAccessNode node) { - if (coAtf.isOwningCollectionField(node.getTree())) { + if (coAtf.isOwningCollectionField(node.getElement())) { String receiverString = receiverAsString(node); boolean isSelfAccess = "this".equals(receiverString); boolean isSuperAccess = "super".equals(receiverString); @@ -1386,7 +1388,7 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg, ReturnNode nod return false; } else { // return is @OwningCollection. Report error if @OwningCollection field. - if (coAtf.isOwningCollectionField(node.getResult().getTree())) { + if (coAtf.isOwningCollectionField(TreeUtils.elementFromTree(node.getResult().getTree()))) { checker.reportError( node.getTree(), "transfer.owningcollection.field.ownership", @@ -1850,7 +1852,7 @@ private void checkReassignmentToOwningCollectionField( // but if rhs is owning, demand CreatesMustCallFor("this") if (rhsCoType == CollectionOwnershipType.OwningCollection) { checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree); - if (coAtf.isOwningCollectionField(lhs.getTree())) { + if (coAtf.isOwningCollectionField(TreeUtils.elementFromTree(lhs.getTree()))) { Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); for (Obligation obligation : obligationsForVar) { obligations.remove(obligation); From 01ae15cd8429003dbad748a5f8232c502a995fb3 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 15:48:56 +0200 Subject: [PATCH 203/374] add missing @param --- .../checker/collectionownership/CollectionOwnershipTransfer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 83029ef3c644..82cb9df15250 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -51,6 +51,7 @@ public class CollectionOwnershipTransfer * Create a CollectionOwnershipTransfer. * * @param analysis the analysis + * @param checker the checker */ public CollectionOwnershipTransfer( CollectionOwnershipAnalysis analysis, CollectionOwnershipChecker checker) { From d7e3c0ce40b5f3e585d45f23d4832fa06bce441a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 27 Jun 2025 15:49:10 +0200 Subject: [PATCH 204/374] fix crash due to receiver not in store --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index ba81fb304fbb..0266c31becf3 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -811,6 +811,10 @@ private void addObligationsForCreatesCollectionObligationAnno( if (hasCreatesCollectionObligation) { Node receiverNode = node.getTarget().getReceiver(); receiverNode = removeCastsAndGetTmpVarIfPresent(receiverNode); + boolean receiverIsResourceCollection = coAtf.isResourceCollection(receiverNode.getTree()); + if (!receiverIsResourceCollection) { + return; + } boolean receiverIsOwningField = coAtf.isOwningCollectionField(TreeUtils.elementFromTree(receiverNode.getTree())); From 620b82eb5f87bccdc2500803163910c4f973c96c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 01:38:20 +0200 Subject: [PATCH 205/374] manual update --- docs/manual/resource-leak-checker.tex | 408 +++++++++++++++++++++++--- 1 file changed, 372 insertions(+), 36 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index a4511a71c018..fbcc95d848dd 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -140,6 +140,26 @@ \end{description} +\begin{description} + +The Resource Leak Checker also supports collections of resources. The following annotations express ownership over resource collections. + +\item[\refqualclass{checker/collectionownership/qual}{OwningCollection}] +\item[\refqualclass{checker/collectionownership/qual}{NotOwningCollection}] + expresses ownership over resource collections. Resource collection formal parameters default to \texttt{@NotOwningCollection}, while resource collection fields and return types default to \texttt{@OwningCollection}. + For more details, see Section~\ref{resource-leak-collections}. + +\end{description} + +\begin{description} + +This annotation is for dedicated destructor methods of resource collection fields. + +\item[\refqualclasswithparams{checker/collectionownership/qual}{CollectionFieldDestructor}{String[] value}] + expresses that the annotated method fulfills the obligations of the given resource collection fields. + For more details, see Section~\ref{resource-leak-collections-fields}. + +\end{description} \sectionAndLabel{Example of how safe resource usage is verified}{resource-leak-example} @@ -626,46 +646,362 @@ \sectionAndLabel{Collections of resources}{resource-leak-collections} -The Resource Leak Checker cannot verify code that stores a collection of -resources in a generic collection (e.g., \) and then -resolves the obligations of each element of the collection at once (e.g., -by iterating over the \). In the future, the checker will support -this. The remainder of this section explains the implementation issues; -most users can skip it. - -The first implementation issue is that \<@Owning> and \<@NotOwning> are -declaration annotations rather than type qualifiers, so they cannot be -written on type arguments. It is possible under the current design to have -an \code{@Owning List}, but not a \code{List<@Owning Socket>}. -It would be better to make \<@Owning> a type annotation, but this is a -challenging design problem. - -The second implementation issue is the defaulting rule for \<@MustCall> on -type variable upper bounds. Currently, this default is \<@MustCall(\{\})>, -which prevents many false positives in code with type variables that makes -no use of resources --- an important design principle. -However, this defaulting rule does have an unfortunate consequence: it is -an error to write \code{List} or any other type with a concrete -type argument where the type argument itself isn't \<@MustCall(\{\})>. A programmer who -needs to write such a type while using the Resource Leak Checker has a few -choices, all of which have some downsides: +The Resource Leak Checker handles homogeneous collections of resources. In a homogeneous collection, every element +has exactly the same must-call and called-methods properties. \texttt{java.util.Iterable} and all its implementations, and \texttt{java.util.Iterator}s are supported; this section +calls those ``collections''. + +The Resource Leak Checker tracks every allocated resource collection in two ways: + +\begin{itemize} + \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - they can't be used to add or remove elements to and from the collection respectively for example. + \item a complementary dataflow analysis tracks an obligation for each allocated resource collection. The obligation can be passed on to method parameters, fields, or return values for example, just like the ownership. The difference is the imprecision - the type system tracks at most one owner per resource collection, while the dataflow analysis tracks at least one obligation per resource collection to ensure it is definitely fulfilled. +\end{itemize} + +What the checker effectively verifies is that any resource collection is properly disposed of. That is, there is some loop that definitely calls the required methods on all elements of the collection. + +For the purposes of this checker, \textit{resource collections} are precisely defined, as the objects of interest for both the type system and obligation tracking. A Java expression is a \textit{resource collection} if it is: + +\begin{enumerate} + \item A \texttt{java.util.Iterable} or implementation thereof (which includes the entire Java Collections framework), or a \texttt{java.util.Iterator}, and: + \item Its type parameter upper bound has non-empty \texttt{MustCall} type. +\end{enumerate} + +For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collection, but one of type \texttt{Set} is not. + +\subsectionAndLabel{Ownership Type System for Resource Collections}{resource-leak-collections-ownership-types} +Of the two tracking mechanisms described above, the ownership type system is more visible from the user perspective. The type hierarchy is the following: + +\begin{verbatim} + @NotOwningCollection + | + @OwningCollection + | + @OwningCollectionWithoutObligation + | + @OwningCollectionBottom +\end{verbatim} \begin{itemize} -\item Write \code{List}. This rejects calls to \ -or other methods that require an instance of the type variable, but it -preserves some of the behavior (e.g., calls to \ are permitted). -This is the best choice most of the time if the \ is not intended to -be owning. -\item Write \code{List<@MustCall Socket>}. This makes it an error to -add a Socket to the list, since the type of the Socket is -\<@MustCall("close")> but the list requires \<@MustCall()>. -\item Suppress one or more warnings. + \item \textbf{@OwningCollectionBottom} is the type of non-\textit{resource collections}. It is thus the type the vast majority of expressions have. + \item \textbf{@OwningCollectionWithoutObligation} is the type of \textit{resource collection} references that definitely own the underlying collection, but also definitely don't have any open calling obligations on their elements. For example, a freshly allocated resource collection is of this type. + \item \textbf{@OwningCollection} is the type of resource collection references that definitely own the underlying collection, but might have open calling obligations for their elements. + \item \textbf{@NotOwningCollection} is the type of resource collection references that might not own the underlying resource collection. \end{itemize} -The recommended way to use the Resource Leak Checker in this situation is -to rewrite the code to avoid a \ of owning resources. If rewriting is -not possible, the programmer will probably need to suppress a warning and -then verify the code using a method other than the Resource Leak Checker. +Only \texttt{@OwningCollection} and \texttt{@NotOwningCollection} are permitted as user-written annotations. The other types are only used internally. + +\subsectionAndLabel{Ownership Types in Action: Defaults and Ownership Transfer}{resource-leak-collections-types-in-action} + +A freshly allocated resource collection always defaults to \texttt{@OwningCollectionWithoutObligation}. + +\begin{Verbatim} +List sockets = new ArrayList<>(); +// `sockets` is now @OwningCollectionWithoutObligation +\end{Verbatim} + +When you now add elements into this collection, its type unrefines to \texttt{@OwningCollection}. + +\begin{Verbatim} +List sockets = new ArrayList<>(); +// `sockets` is @OwningCollectionWithoutObligation +for (int i = 0; i < n; i++) { + sockets.add(new Socket(``myHost'', myPort + i)); + // `sockets` is @OwningCollection +} +// `sockets` is @OwningCollection +\end{Verbatim} + +All methods called on a resource collection that potentially move an element with a calling obligation into the collection change the type of the receiving collection to \texttt{@OwningCollection} and create a tracked obligation for the collection in the dataflow analysis. + +For method parameters, the default type of resource collection references is \texttt{@NotOwningCollection}. + +\begin{Verbatim} +void m(Iterable sockets) { + // `sockets` is @NotOwningCollection + ... +} +\end{Verbatim} + +This means that by default, a resource collection parameter can only do certain 'safe' operations on the collection that do not add, remove, or overwrite existing elements. But it also means that the parameter does not have any calling obligation for the elements of the collection, and it can accept both owning and not owning collection references due to normal subtyping rules. If an \texttt{OwningCollection} reference is passed to an unannotated parameter, the obligation stays at the call-site. + +Annotating the parameter with \texttt{@OwningCollection} overrides this behavior. In this case, the ownership is transferred from the call-site argument to the method parameter. Concretely, this means that the call-site obligation is removed and the parameter takes on the responsibility of disposing of the collection. The type of the argument at call-site is changed to \texttt{@NotOwningCollection} to maintain the invariant that there's at most one owning reference for each resource collection. + +\begin{Verbatim} +void m(@OwningCollection Iterable sockets) { + // `sockets` is @OwningCollection + ... + // this loop disposes of the collection and fulfills the obligation + for (Socket s : sockets) { + try { + s.close(); + } catch (Exception e) { + System.out.println(e.stackTrace()); + } + } + // `sockets` is @OwningCollectionWithoutObligation again +} +\end{Verbatim} + +Resource collection return types default to \texttt{@OwningCollection}. Returning a resource collection to such a return type fulfills the obligation for the method. The obligation and ownership are both transferred to the call-site return expression. + +\begin{verbatim} +List returnSocketList() { + List socketList = new ArrayList<>(); + // `socketList` is @OwningCollectionWithoutObligation + socketList.add(new Socket(myHost, myPort)); + // `socketList` is @OwningCollection + return socketList; + // the return transfers obligation and ownership to the call-site +} +\end{verbatim} + +Writing \texttt{@NotOwningCollection} on the return type overrides this default. In this case, ownership and obligation are not transferred to the call-site and the obligation must be fulfilled or passed on prior to the return. + +Ownership is also transferred through assignments. If the right-hand side of an assignment is of type \texttt{@OwningCollection} or \texttt{@OwningCollectionWithoutObligation}, the ownership is transferred to the left-hand side expression, and thus the right-hand side is unrefined to \texttt{@NotOwningCollection} to maintain the invariant of having at most one owning reference per resource collection. + +\begin{verbatim} +// `r` is @OwningCollection +l = r; +// `l` is now @OwningCollection, `r` is @NotOwningCollection +\end{verbatim} + +Just like the ownership, the obligation is also transferred to the left-hand side. + +\subsectionAndLabel{Fulfilling Collection Obligations}{resource-leak-collections-fulfillment} + +To fulfill the obligation of a resource collection, the obligation can be passed on, but at some point it has to be fulfilled by calling the required methods on its elements. Recommended are enhanced \texttt{for} loops, but indexed \texttt{for} loops are also supported with some syntactic restrictions. + +Here is an example of such a loop: + +\begin{verbatim} +// `socketList` is @OwningCollection and has an obligation for calling `close` on its elements +for (Socket s : socketList) { + try { + s.close(); + } catch (Exception e) { + e.printStackTrace(); + } +} +// `socketList` is @OwningCollectionWithoutObligation and has no obligation +\end{verbatim} + +The main effect such a loop has is that the obligations corresponding to the methods it calls on all elements are removed. If the loop calls all required methods - which is almost always the case, since classes usually have at most one \texttt{MustCall} method - the type of the collection is additionally refined to \texttt{@OwningCollectionWithoutObligation}. + +To determine the methods such a loop calls, the checker does the following things: + +\begin{itemize} + \item It checks that the loop does not terminate early. + \item It leverages the \texttt{CalledMethods} analysis to determine the methods definitely called on the loop iterator variable. This means that anything that changes the \texttt{CalledMethods} type of the iterator variable is supported, for example passing it as an argument to a method annotated \texttt{@EnsuresCalledMethods(``m'')}, or using try-with constructs. Nullness checks are also supported. +\end{itemize} + +If an indexed \texttt{for} loop is used, the checker must additionally check that the loop does in fact iterate over all elements of the collection, and that the loop doesn't write to the loop variable for example. The checks are the following: + +\begin{itemize} + \item Loop iteration bounds: + \begin{itemize} + \item The lower bound of iteration is 0, and + \item The upper bound of iteration is \texttt{c.size()} + \item The loop's increment expression is a pre- or post-increment of the loop variable. + \item The collection element is accessed using \texttt{collection.get(i)}, where \texttt{i} is the loop variable. + \end{itemize} + \item The loop does not assign to the collection variable. + \item The loop does not assign to collection elements. +\end{itemize} + +Here is an example using an indexed \texttt{for} loop. + +\begin{verbatim} +for (int i = 0; i < socketList.size(); i++) { + try { + socketList.get(i).close(); + } catch (Exception e) {} +} +\end{verbatim} + +\subsectionAndLabel{Resource Collection Fields}{resource-leak-collections-fields} +By default, resource collection fields are owned by the enclosing class. Static fields are not supported and an error is reported for a declaration of a static resource collection field. + +To verify a resource collection field \textit{field}, the checker looks for the following: +\begin{enumerate} + \item A \texttt{@MustCall} annotation on the enclosing class. In this case, the class is of type \texttt{@MustCall(``close'')} by subclassing Closeable. + \item Among the methods in the \texttt{@MustCall} type of the enclosing class (usually there's just one), the checker now looks for the post-condition annotation \texttt{@CollectionFieldDestructor(``field'')}. + \item The annotation \texttt{@CollectionFieldDestructor(``field'')} asserts that at the method exit, \textit{field} has type \texttt{@OwningCollectionWithoutObligation}, which the checker verifies. + \item Methods that call 'unsafe' methods creating obligations for the resource collection field, such as \texttt{socketList.add(Socket)} in this case, must have a \texttt{@CreatesMustCallFor(``this'')} annotation. +\end{enumerate} + +This now shifts the burden to the client of the \texttt{Aggregator} class to call \texttt{close()} on instances before they leave scope. + +Consider the following example: + +\begin{verbatim} +class Aggregator extends Closeable { + List socketList = new ArrayList<>(); + + public Aggregator() {} + + public Aggregator(@OwningCollection List l) { + // `this.socketList` is @OwningCollectionWithoutObligation + this.socketList = l; + // `this.socketList` is @OwningCollection + // the obligation of `l` has been resolved + } + + @CreatesMustCallFor(''this'') + public void add(@Owning Socket s) { + // this call creates an obligation for the field. + // thus, the enclosing method must have a @CreatesMustCallFor(''this'') annotation. + socketList.add(s); + } + + @Override + @CollectionFieldDestructor(''socketList'') + public void close() { + for (Socket s : socketList) { + try { + s.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} +\end{verbatim} + +If a resource collection field is final, the checker doesn't have to verify assignments to it. If it is not final, the check verifies assignments in the following way: +\begin{itemize} + \item Assignments in initializer blocks are not permitted. + \item Assignments in a declaration initializer are permitted if the right-hand side is at most \texttt{@OwningCollectionWithoutObligation}. + \item Thanks to the above two restrictions, it can be assumed that an owned resource collection field is \texttt{@OwningCollectionWithoutObligation} at the beginning of a constructor. An assignment to the field is now allowed if the field is \texttt{@OwningCollectionWithoutObligation} just before the assignment. This condition holds in any other (non-constructor) method too, but the field is \texttt{@OwningCollection} at the method entrance, unlike for a constructor. A legal assignment to an owning resource collection field resolves the obligation of the right-hand side. +\end{itemize} + +Resource collection fields owned by the enclosing class are special cases for ownership transfer, as the checker doesn't allow transfering the ownership away from such a field. If for example a member method were able to transfer the ownership of the field to an external method parameter, the checker would have to track that the field is no longer the owner and set its type to \texttt{@NotOwningCollection} for future method invocations on the class instance. For the sake of simplicity, the checker doesn't track this and thus doesn't allow ownership transfers for fields. In particular: + +\begin{itemize} + \item The field cannot be passed as an argument to an \texttt{@OwningCollection} parameter. + \item The field cannot be returned to an \texttt{@OwningCollection} return type. + \item If the field is on the right-hand side of an assignment, the left-hand side has type \texttt{@NotOwningCollection} after the assignment, so as to keep the at-most-one-owner invariant. + \item Any access to the field from outside of the class (or a subclass) is not permitted. +\end{itemize} + +If the enclosing class is not intended to take responsibility for the resource collection field, annotate it with @NotOwningCollection. It now behaves just like any other resource collection reference. In particular: + +\begin{itemize} + \item Assigning to such a field does not resolve the obligation of the right-hand side. + \item The enclosing class does not have to fulfill any obligations on the field. +\end{itemize} + +\subsectionAndLabel{Iterators over Resource Collections}{resource-leak-collections-iterators} +Iterators are frequently used to traverse collections. It is always safe to create an iterator for any non-resource collection. Since an iterator takes a snapshot of the collection at the time it is created, it does not matter what happens with the collection after the iterator is created (else, a \ is thrown for the next usage of the iterator). +An iterator created for a resource collection has the same collection ownership type as the resource collection. + +\begin{verbatim} +// `socketList` is @OwningCollectionWithoutObligation +Iterator socketIterator = socketList.iterator(); +// `socketIterator` is @OwningCollectionWithoutObligation +\end{verbatim} + +The only concering operation an iterator can do is \texttt{remove()}, as the removed element might have open calling obligations. + +Just like a \texttt{java.util.Collection} of type \texttt{@NotOwningCollection}, an iterator of this type is not allowed to call \texttt{remove()} at all. Iterators of type \texttt{@OwningCollectionBottom} and \texttt{@OwningCollectionWithoutObligation} can always call \texttt{remove()}. The interesting remaining case is that of an \texttt{@OwningCollection} iterator. +Such an iterator and the values its calls to \texttt{next()} return are tracked with special obligations. This is to ensure that whenever an \texttt{@OwningCollection} iterator calls \texttt{close()}, the value returned by the previous call to \texttt{Iterator.next()} has its \texttt{MustCall} obligations fulfilled. + +Here is an example of unsafe iterator usage: + +\begin{verbatim} +List foo(@OwningCollection List list) { + Iterator iter = list.iterator(); + // `iter` is @OwningCollection and must be tracked + iter.next(); + iter.remove(); // unsafe! + return list; +} +\end{verbatim} + +\begin{verbatim} + error: [required.method.not.called] @MustCall method close may not have been invoked on iter.next() or any of its aliases. + iter.next(); + ^ +\end{verbatim} + +The removed element must have its must-call obligations fulfilled. Here is an example of safe usage: + +\begin{verbatim} +List foo(@OwningCollection List list) { + Iterator iter = list.iterator(); + // `iter` is @OwningCollection and must be tracked + try { + iter.next().close(); + } catch (Exception e) { + } + iter.remove(); + return list; +} +\end{verbatim} + +The returned element may also be stored in a variable first and the fulfillment of the obligation of the returned element may also occur after the call to \texttt{remove()}: + +\begin{verbatim} +List foo(@OwningCollection List list) { + Iterator iter = list.iterator(); + // `iter` is @OwningCollection and must be tracked + Socket s = iter.next(); + iter.remove(); + try { + s.close(); + } catch (Exception e) { + } + return list; +} +\end{verbatim} + +The returned element by \texttt{iter.next()} may have its must-call obligation fulfilled in any way described by the resource leak checker, even by transfering its ownership (for example by storing it in a field or passing it as an \texttt{@OwningCollection} method argument). + +Iterators over collections with obligations may also be returned, passed as method or constructor arguments and even be stored in fields. + +\subsectionAndLabel{JDK Method Signatures}{resource-leak-collections-jdk-methods} +[[TODO: Add a table with a number of collection/iterable methods and their collection ownership annotations]] + + +% The Resource Leak Checker cannot verify code that stores a collection of +% resources in a generic collection (e.g., \) and then +% resolves the obligations of each element of the collection at once (e.g., +% by iterating over the \). In the future, the checker will support +% this. The remainder of this section explains the implementation issues; +% most users can skip it. + +% The first implementation issue is that \<@Owning> and \<@NotOwning> are +% declaration annotations rather than type qualifiers, so they cannot be +% written on type arguments. It is possible under the current design to have +% an \code{@Owning List}, but not a \code{List<@Owning Socket>}. +% It would be better to make \<@Owning> a type annotation, but this is a +% challenging design problem. + +% The second implementation issue is the defaulting rule for \<@MustCall> on +% type variable upper bounds. Currently, this default is \<@MustCall(\{\})>, +% which prevents many false positives in code with type variables that makes +% no use of resources --- an important design principle. +% However, this defaulting rule does have an unfortunate consequence: it is +% an error to write \code{List} or any other type with a concrete +% type argument where the type argument itself isn't \<@MustCall(\{\})>. A programmer who +% needs to write such a type while using the Resource Leak Checker has a few +% choices, all of which have some downsides: + +% \begin{itemize} +% \item Write \code{List}. This rejects calls to \ +% or other methods that require an instance of the type variable, but it +% preserves some of the behavior (e.g., calls to \ are permitted). +% This is the best choice most of the time if the \ is not intended to +% be owning. +% \item Write \code{List<@MustCall Socket>}. This makes it an error to +% add a Socket to the list, since the type of the Socket is +% \<@MustCall("close")> but the list requires \<@MustCall()>. +% \item Suppress one or more warnings. +% \end{itemize} + +% The recommended way to use the Resource Leak Checker in this situation is +% to rewrite the code to avoid a \ of owning resources. If rewriting is +% not possible, the programmer will probably need to suppress a warning and +% then verify the code using a method other than the Resource Leak Checker. \sectionAndLabel{Resource Leak Checker annotation inference algorithm}{resource-leak-checker-inference-algo} From 076f0b7208a759df690ad461bbf62f91d7b44cf5 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 14:39:15 +0200 Subject: [PATCH 206/374] fix rlc-collections manual --- docs/manual/resource-leak-checker.tex | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index fbcc95d848dd..2f6e5d90e424 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -140,10 +140,10 @@ \end{description} -\begin{description} The Resource Leak Checker also supports collections of resources. The following annotations express ownership over resource collections. +\begin{description} \item[\refqualclass{checker/collectionownership/qual}{OwningCollection}] \item[\refqualclass{checker/collectionownership/qual}{NotOwningCollection}] expresses ownership over resource collections. Resource collection formal parameters default to \texttt{@NotOwningCollection}, while resource collection fields and return types default to \texttt{@OwningCollection}. @@ -151,10 +151,9 @@ \end{description} -\begin{description} - This annotation is for dedicated destructor methods of resource collection fields. +\begin{description} \item[\refqualclasswithparams{checker/collectionownership/qual}{CollectionFieldDestructor}{String[] value}] expresses that the annotated method fulfills the obligations of the given resource collection fields. For more details, see Section~\ref{resource-leak-collections-fields}. @@ -668,7 +667,7 @@ For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collection, but one of type \texttt{Set} is not. -\subsectionAndLabel{Ownership Type System for Resource Collections}{resource-leak-collections-ownership-types} +\subsectionAndLabel{Ownership type system for resource collections}{resource-leak-collections-ownership-types} Of the two tracking mechanisms described above, the ownership type system is more visible from the user perspective. The type hierarchy is the following: \begin{verbatim} @@ -690,7 +689,7 @@ Only \texttt{@OwningCollection} and \texttt{@NotOwningCollection} are permitted as user-written annotations. The other types are only used internally. -\subsectionAndLabel{Ownership Types in Action: Defaults and Ownership Transfer}{resource-leak-collections-types-in-action} +\subsectionAndLabel{Ownership types in action: defaults and ownership transfer}{resource-leak-collections-types-in-action} A freshly allocated resource collection always defaults to \texttt{@OwningCollectionWithoutObligation}. @@ -767,7 +766,7 @@ Just like the ownership, the obligation is also transferred to the left-hand side. -\subsectionAndLabel{Fulfilling Collection Obligations}{resource-leak-collections-fulfillment} +\subsectionAndLabel{Fulfilling collection obligations}{resource-leak-collections-fulfillment} To fulfill the obligation of a resource collection, the obligation can be passed on, but at some point it has to be fulfilled by calling the required methods on its elements. Recommended are enhanced \texttt{for} loops, but indexed \texttt{for} loops are also supported with some syntactic restrictions. @@ -818,7 +817,7 @@ } \end{verbatim} -\subsectionAndLabel{Resource Collection Fields}{resource-leak-collections-fields} +\subsectionAndLabel{Resource collection fields}{resource-leak-collections-fields} By default, resource collection fields are owned by the enclosing class. Static fields are not supported and an error is reported for a declaration of a static resource collection field. To verify a resource collection field \textit{field}, the checker looks for the following: @@ -890,7 +889,7 @@ \item The enclosing class does not have to fulfill any obligations on the field. \end{itemize} -\subsectionAndLabel{Iterators over Resource Collections}{resource-leak-collections-iterators} +\subsectionAndLabel{Iterators over resource collections}{resource-leak-collections-iterators} Iterators are frequently used to traverse collections. It is always safe to create an iterator for any non-resource collection. Since an iterator takes a snapshot of the collection at the time it is created, it does not matter what happens with the collection after the iterator is created (else, a \ is thrown for the next usage of the iterator). An iterator created for a resource collection has the same collection ownership type as the resource collection. From b9bf41079791667ce74f9d031c3a2db95df2469c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 14:46:40 +0200 Subject: [PATCH 207/374] fix quotes --- docs/manual/resource-leak-checker.tex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 2f6e5d90e424..efc20d31f372 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -704,7 +704,7 @@ List sockets = new ArrayList<>(); // `sockets` is @OwningCollectionWithoutObligation for (int i = 0; i < n; i++) { - sockets.add(new Socket(``myHost'', myPort + i)); + sockets.add(new Socket(myHost, myPort + i)); // `sockets` is @OwningCollection } // `sockets` is @OwningCollection @@ -845,15 +845,15 @@ // the obligation of `l` has been resolved } - @CreatesMustCallFor(''this'') + @CreatesMustCallFor("this") public void add(@Owning Socket s) { // this call creates an obligation for the field. - // thus, the enclosing method must have a @CreatesMustCallFor(''this'') annotation. + // thus, the enclosing method must have a @CreatesMustCallFor("this") annotation. socketList.add(s); } @Override - @CollectionFieldDestructor(''socketList'') + @CollectionFieldDestructor("socketList") public void close() { for (Socket s : socketList) { try { From fcf9e05d8759e53a37fbfe65b68a8c1d0241efd9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 15:18:48 +0200 Subject: [PATCH 208/374] suppress this-escape warning for calling postInit() --- .../CollectionOwnershipAnnotatedTypeFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f2b052dcf2b1..6051c0a4adc6 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -167,6 +167,7 @@ public static PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Blo * * @param checker the checker associated with this type factory */ + @SuppressWarnings("this-escape") public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); NOTOWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, NotOwningCollection.class); From fab3eb967b143f21732785a9af311b7c84dfbe2e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 17:04:48 +0200 Subject: [PATCH 209/374] fix nullness crash --- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 6051c0a4adc6..622e983a539a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -250,6 +250,7 @@ public void postAnalyze(ControlFlowGraph cfg) { * @return whether t is a resource collection */ public boolean isResourceCollection(TypeMirror t) { + if (t == null) return false; List list = getMustCallValuesOfResourceCollectionComponent(t); return list != null && list.size() > 0; } @@ -330,6 +331,7 @@ public boolean isOwningCollectionParameter(Element elt) { * @return whether the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { + if (tree == null) return false; MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); AnnotatedTypeMirror treeMcType = mcAtf.getAnnotatedType(tree); List list = getMustCallValuesOfResourceCollectionComponent(treeMcType); From dd928962921c08d8232da8e2719e6daa586054f2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 28 Jun 2025 17:05:12 +0200 Subject: [PATCH 210/374] make @OCBottom and @OCwO illegal manual annotations --- .../CollectionOwnershipVisitor.java | 12 ++++++++++++ .../checker/collectionownership/messages.properties | 1 + .../CollectionOwnershipBasicTyping.java | 1 + .../CollectionOwnershipDefaults.java | 2 ++ .../LoopBodyAnalysisTest.java | 2 ++ 5 files changed, 18 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 6116c931f2c4..f3e75025ef45 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership; +import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.VariableTree; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -14,6 +15,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -34,6 +36,16 @@ public CollectionOwnershipVisitor(BaseTypeChecker checker) { super(checker); } + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + AnnotationMirror am = TreeUtils.annotationFromAnnotationTree(tree); + if (AnnotationUtils.areSame(am, atypeFactory.BOTTOM) + || AnnotationUtils.areSame(am, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION)) { + checker.reportError(tree, "illegal.type.annotation", tree); + } + return super.visitAnnotation(tree, p); + } + /** * This method typically issues a warning if the result type of the constructor is not top, * because in top-default type systems that indicates a potential problem. The Must Call Checker diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index da5750cc646e..f70a6d4bfa28 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,2 +1,3 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. +illegal.type.annotation=The annotation %s is for internal use only and not an allowed user annotation. diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 8b0c01158629..8a60286f845b 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -130,6 +130,7 @@ void checkArgIsOwning( @OwningCollection Collection collection) {} void checkArgIsOCwoO( + // :: error: illegal.type.annotation @OwningCollectionWithoutObligation Collection collection) {} } diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index 5d582b91237d..c0367ca0e363 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -138,8 +138,10 @@ void checkArgIsOwning( void checkArgIsOwning(Socket @OwningCollection [] collection) {} void checkArgIsOCwoO( + // :: error: illegal.type.annotation @OwningCollectionWithoutObligation Collection collection) {} + // :: error: illegal.type.annotation void checkArgIsOCwoO(Socket @OwningCollectionWithoutObligation [] collection) {} } diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 566ce8c72daf..ae2b399fb223 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -168,7 +168,9 @@ void indexForLoopList(@OwningCollection List resources) { } } + // :: error: illegal.type.annotation void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} + // :: error: illegal.type.annotation void checkArgIsOCWO(Resource @OwningCollectionWithoutObligation [] arg) {} } From 56e8fd416cb669bf9f781c9c33572cfee025f59c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 5 Jul 2025 18:12:43 +0200 Subject: [PATCH 211/374] disallow owning rc field assignments with @OC rhs --- .../MustCallConsistencyAnalyzer.java | 37 +++++++++++++------ .../checker/resourceleak/messages.properties | 1 + 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 0266c31becf3..af4546fabac2 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1807,15 +1807,30 @@ private void checkReassignmentToOwningCollectionField( // The assignment is taking place in a variable declaration's // initializer or in an initializer block. if (node.getTree() instanceof VariableTree) { - // assignment is a field initializer. Is always permitted. - // switch (rhsCoType) { - // case OwningCollectionWithoutObligation: - // break; - // case OwningCollection: - // case NotOwningCollection: - // throw new BugInCF("rhs of field initializer is " + rhsCoType + ": " + rhs); - // default: - // } + // assignment is a field initializer. Permitted only if RHS is @OwningCollectionWithoutObligation. + CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipType rhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); + if (rhsCoType == null) { + throw new BugInCF( + "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); + } + switch (rhsCoType) { + case OwningCollectionWithoutObligation: + break; + case OwningCollection: + case NotOwningCollection: + checker.reportError( + node.getTree(), + "illegal.owningcollection.field.assignment", + rhsCoType.toString() + ); + break; + case OwningCollectionBottom: + throw new BugInCF( + "Expression " + + node + + " has resource collection operand, but @OwningCollectionBottom type."); + } return; } else { // is an initialization block. Not supported. @@ -1829,12 +1844,12 @@ private void checkReassignmentToOwningCollectionField( } else { // The assignment is taking place in a (possibly constructor) method. CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); - CollectionOwnershipType lhsCoType = coAtf.getCoType(getTempVarOrNode(lhs), coStore); + CollectionOwnershipType lhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); if (lhsCoType == null) { throw new BugInCF( "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); } - CollectionOwnershipType rhsCoType = coAtf.getCoType(getTempVarOrNode(rhs), coStore); + CollectionOwnershipType rhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); if (rhsCoType == null) { throw new BugInCF( "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index 46103530179d..52fc11950a32 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -11,3 +11,4 @@ required.method.not.known=The checker cannot determine the must call methods of unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field owned by a different class. +illegal.owningcollection.field.assignment=The right-hand side of a field initializer assignment to an owning resource collection field must be of type @OwningCollectionWithoutObligation, but is of type %s. Consider moving the assignment into the constructor. From 627cc6e82b7a213b3b15ac46d9cc9bc8c68e44ba Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 5 Jul 2025 20:40:51 +0200 Subject: [PATCH 212/374] keep @NotOwningCollection rc fields in store as well --- .../CollectionOwnershipAnnotatedTypeFactory.java | 16 ++++++++++++++++ .../CollectionOwnershipStore.java | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 622e983a539a..a6bdb89b3204 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -286,6 +286,22 @@ public boolean isOwningCollectionField(Element elt) { return false; } + /** + * Whether the given element is a resource collection field. + * + * @param elt the element + * @return true if the element is a resource collection field. + */ + public boolean isResourceCollectionField(Element elt) { + if (elt == null) return false; + if (isResourceCollection(elt.asType())) { + if (elt.getKind().isField()) { + return true; + } + } + return false; + } + /** * Whether the given element is a resource collection parameter that is {@code @OwningCollection} * by declaration, which is the default behavior, i.e. with no different collection ownership diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 0d5846b38b0b..28afc44dc38f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -50,7 +50,7 @@ protected CFValue newFieldValueAfterMethodCall( CFValue value) { CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value); if (superResult == null) { - if (atypeFactory.isOwningCollectionField(fieldAccess.getField())) { + if (atypeFactory.isResourceCollectionField(fieldAccess.getField())) { return value; } } From 0a7d4f2e2bbc7ce7708b521fa84b337dad6088cb Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 5 Jul 2025 20:41:55 +0200 Subject: [PATCH 213/374] fix how return statements are checked for @OC return vals --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index af4546fabac2..4f40047c9832 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -772,8 +772,8 @@ public void analyze(ControlFlowGraph cfg) { private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); if (tmpVar != null) { - CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); - CollectionOwnershipType cotype = coAtf.getCoType(node, coStore); + CollectionOwnershipStore coStore = coAtf.getStoreAfter(node); + CollectionOwnershipType cotype = coAtf.getCoType(tmpVar, coStore); if (cotype == CollectionOwnershipType.OwningCollection) { ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), node.getTree()); From ffd8f250b45963d66d030c6e0223a65add0b93b0 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 5 Jul 2025 20:42:22 +0200 Subject: [PATCH 214/374] allow assignments to final field --- .../MustCallConsistencyAnalyzer.java | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4f40047c9832..1ec0225a17a8 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -34,6 +34,7 @@ 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; import javax.lang.model.type.TypeKind; @@ -1796,6 +1797,16 @@ private void checkReassignmentToOwningCollectionField( Node lhs = node.getTarget(); Node rhs = node.getExpression(); + // assignment to final field allowed + if (lhs != null && lhs.getTree() != null) { + Element lhsElt = TreeUtils.elementFromTree(lhs.getTree()); + if (lhsElt != null) { + if (lhsElt.getModifiers().contains(Modifier.FINAL)) { + return; + } + } + } + if (!coAtf.isResourceCollection(lhs.getType()) && !coAtf.isResourceCollection(rhs.getType())) { return; } @@ -1807,10 +1818,16 @@ private void checkReassignmentToOwningCollectionField( // The assignment is taking place in a variable declaration's // initializer or in an initializer block. if (node.getTree() instanceof VariableTree) { - // assignment is a field initializer. Permitted only if RHS is @OwningCollectionWithoutObligation. + // assignment is a field initializer. Permitted only if RHS is + // @OwningCollectionWithoutObligation + // or null. CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); - CollectionOwnershipType rhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); + CollectionOwnershipType rhsCoType = + coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); if (rhsCoType == null) { + if (TreeUtils.isNullExpression(rhs.getTree())) { + return; + } throw new BugInCF( "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); } @@ -1820,16 +1837,9 @@ private void checkReassignmentToOwningCollectionField( case OwningCollection: case NotOwningCollection: checker.reportError( - node.getTree(), - "illegal.owningcollection.field.assignment", - rhsCoType.toString() - ); + node.getTree(), "illegal.owningcollection.field.assignment", rhsCoType.toString()); break; case OwningCollectionBottom: - throw new BugInCF( - "Expression " - + node - + " has resource collection operand, but @OwningCollectionBottom type."); } return; } else { @@ -1844,12 +1854,14 @@ private void checkReassignmentToOwningCollectionField( } else { // The assignment is taking place in a (possibly constructor) method. CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); - CollectionOwnershipType lhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); + CollectionOwnershipType lhsCoType = + coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); if (lhsCoType == null) { throw new BugInCF( "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); } - CollectionOwnershipType rhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); + CollectionOwnershipType rhsCoType = + coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); if (rhsCoType == null) { throw new BugInCF( "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); From d935a351482226b6dc98dd34c9f942fb3b9ecb9d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 13:45:37 +0200 Subject: [PATCH 215/374] disallow static rc fields --- .../CollectionOwnershipVisitor.java | 12 ++++++++++-- .../checker/collectionownership/messages.properties | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index f3e75025ef45..def5769ba88e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -7,6 +7,7 @@ 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.VariableElement; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; @@ -86,8 +87,15 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { @Override public Void visitVariable(VariableTree tree, Void p) { - if (atypeFactory.isOwningCollectionField(TreeUtils.elementFromDeclaration(tree))) { - checkOwningCollectionField(tree); + Element elt = TreeUtils.elementFromDeclaration(tree); + if (elt != null && atypeFactory.isResourceCollectionField(elt)) { + if (elt.getModifiers().contains(Modifier.STATIC)) { + // error: static resource collection fields not supported + checker.reportError(tree, "static.resource.collection.field", tree); + } + if (atypeFactory.isOwningCollectionField(elt)) { + checkOwningCollectionField(tree); + } } return super.visitVariable(tree, p); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index f70a6d4bfa28..4ea7e9890126 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,3 +1,5 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. illegal.type.annotation=The annotation %s is for internal use only and not an allowed user annotation. +static.resource.collection.field=The resource collection field %s is static, which is not supported by the checker. +The field is unsoundly treated as non-static by the checker. From 23a2fcd68d362912c33eb2c040e2fc2e41bf1166 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 13:46:12 +0200 Subject: [PATCH 216/374] remove obligation of rhs in assignment to owning rc final field lhs --- .../MustCallConsistencyAnalyzer.java | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 1ec0225a17a8..6e348a5d5762 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1797,20 +1797,23 @@ private void checkReassignmentToOwningCollectionField( Node lhs = node.getTarget(); Node rhs = node.getExpression(); - // assignment to final field allowed + if (!coAtf.isResourceCollection(lhs.getType()) && !coAtf.isResourceCollection(rhs.getType())) { + return; + } + + boolean isOwningCollectionField = + coAtf.isOwningCollectionField(TreeUtils.elementFromTree(lhs.getTree())); + boolean isFinalField = false; + if (lhs != null && lhs.getTree() != null) { Element lhsElt = TreeUtils.elementFromTree(lhs.getTree()); if (lhsElt != null) { if (lhsElt.getModifiers().contains(Modifier.FINAL)) { - return; + isFinalField = true; } } } - if (!coAtf.isResourceCollection(lhs.getType()) && !coAtf.isResourceCollection(rhs.getType())) { - return; - } - TreePath currentPath = cmAtf.getPath(node.getTree()); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); @@ -1826,6 +1829,7 @@ private void checkReassignmentToOwningCollectionField( coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); if (rhsCoType == null) { if (TreeUtils.isNullExpression(rhs.getTree())) { + // assignment to null allowed return; } throw new BugInCF( @@ -1833,15 +1837,27 @@ private void checkReassignmentToOwningCollectionField( } switch (rhsCoType) { case OwningCollectionWithoutObligation: - break; + return; case OwningCollection: + // remove obligations of rhs if lhs is @OC field. + // Although only safe if lhs is also final, remove + // obligations to prevent a second error. The illegal + // assignment error below is issued if lhs not final. + if (isOwningCollectionField) { + Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); + for (Obligation obligation : obligationsForVar) { + obligations.remove(obligation); + } + if (isFinalField) return; + } + // fall through case NotOwningCollection: checker.reportError( node.getTree(), "illegal.owningcollection.field.assignment", rhsCoType.toString()); - break; + return; case OwningCollectionBottom: + return; } - return; } else { // is an initialization block. Not supported. checker.reportError( @@ -1854,19 +1870,35 @@ private void checkReassignmentToOwningCollectionField( } else { // The assignment is taking place in a (possibly constructor) method. CollectionOwnershipStore coStore = coAtf.getStoreBefore(node); + CollectionOwnershipType rhsCoType = + coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); + if (rhsCoType == null) { + if (TreeUtils.isNullExpression(rhs.getTree())) { + rhsCoType = CollectionOwnershipType.OwningCollectionBottom; + } else { + throw new BugInCF( + "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); + } + } + + if (isFinalField) { + // special case because final field is not in store before initial assignment. + // final assignment is always allowed. Remove obligations of RHS if field is owning. + if (isOwningCollectionField) { + Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); + for (Obligation obligation : obligationsForVar) { + obligations.remove(obligation); + } + } + return; + } + CollectionOwnershipType lhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); if (lhsCoType == null) { throw new BugInCF( "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); - } - CollectionOwnershipType rhsCoType = - coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(rhs), coStore); - if (rhsCoType == null) { - throw new BugInCF( - "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); - } - if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom + } else if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom || rhsCoType == CollectionOwnershipType.OwningCollectionBottom) { throw new BugInCF( "Expression " @@ -1877,20 +1909,20 @@ private void checkReassignmentToOwningCollectionField( switch (lhsCoType) { case NotOwningCollection: // doesn't own elements. safe to overwrite. - break; + return; case OwningCollectionWithoutObligation: // no obligation. assignment allowed. // but if rhs is owning, demand CreatesMustCallFor("this") if (rhsCoType == CollectionOwnershipType.OwningCollection) { checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree); - if (coAtf.isOwningCollectionField(TreeUtils.elementFromTree(lhs.getTree()))) { + if (isOwningCollectionField) { Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); for (Obligation obligation : obligationsForVar) { obligations.remove(obligation); } } } - break; + return; case OwningCollection: // assignment not allowed checker.reportError( @@ -1899,8 +1931,9 @@ private void checkReassignmentToOwningCollectionField( coAtf.getMustCallValuesOfResourceCollectionComponent(lhs.getTree()).get(0), lhs.getTree(), "Field assignment might overwrite field's current value"); - break; + return; default: + return; } } } From c8d95a0c8ef5e91758f14f402804696d2658ff6a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 13:46:30 +0200 Subject: [PATCH 217/374] improve error message for foreign rc field access --- .../checkerframework/checker/resourceleak/messages.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index 52fc11950a32..d95fe86e7ed6 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -10,5 +10,5 @@ mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. -foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field owned by a different class. +foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field not owned by this class instance. illegal.owningcollection.field.assignment=The right-hand side of a field initializer assignment to an owning resource collection field must be of type @OwningCollectionWithoutObligation, but is of type %s. Consider moving the assignment into the constructor. From 8ddb0427d6437bd9807e83a4759bfb49c0382fac Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 13:46:37 +0200 Subject: [PATCH 218/374] add field test cases --- .../OwningCollectionFieldTyping.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index c7a8f2cff4c6..22b6396e0d41 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -30,6 +30,17 @@ public ConstructorTakesOwnership(@OwningCollection List list, float a) } } + // assignment to @NotOwningCollection field is legal, but + // here the obligation is not fulfilled after reassigning. + public void reassignNotOwningCollectionFieldLegal() { + // :: error: unfulfilled.collection.obligations + notOwningField = getList(); + } + + List getList() { + return new ArrayList(); + } + // no justification for reassignment. Not allowed. public void reassignCollectionFieldIllegal() { // :: error: unfulfilled.collection.obligations @@ -122,3 +133,65 @@ List tryTransferringFieldOwnershipReturn() { return ocField; } } + +class OwningFieldWithIllegalInitializer implements Closeable { + // :: error: illegal.owningcollection.field.assignment + List fieldList = getList(); + + List getList() { + return new ArrayList(); + } + + @CollectionFieldDestructor("fieldList") + public void close() { + for (Resource r : fieldList) { + r.close(); + r.flush(); + } + } +} + +// here, the assignment to an @OwningCollection rhs is allowed +class OwningFinalFieldWithOwningRHSInitializer implements Closeable { + final List fieldList = getList(); + + List getList() { + return new ArrayList(); + } + + @CollectionFieldDestructor("fieldList") + public void close() { + for (Resource r : fieldList) { + r.close(); + r.flush(); + } + } +} + +class OwningFieldWithNullInitializer implements Closeable { + List fieldList = null; + + @CollectionFieldDestructor("fieldList") + public void close() { + for (Resource r : fieldList) { + r.close(); + r.flush(); + } + } +} + +class OwningFinalField implements Closeable { + final List fieldList; + + public OwningFinalField(@OwningCollection List list) { + this.fieldList = list; + } + + @CollectionFieldDestructor("fieldList") + public void close() { + for (Resource r : fieldList) { + r.close(); + r.flush(); + } + } +} From a62bede0962f83353e7eb5dc99d969fe4cdce8c8 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 15:40:08 +0200 Subject: [PATCH 219/374] remove array support --- ...llectionOwnershipAnnotatedTypeFactory.java | 98 +++---------------- .../checker/mustcall/MustCallVisitor.java | 68 +++---------- .../MustCallConsistencyAnalyzer.java | 98 ------------------- .../RLCCalledMethodsAnnotatedTypeFactory.java | 42 +------- 4 files changed, 30 insertions(+), 276 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index a6bdb89b3204..6507df83baf8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -3,7 +3,6 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; @@ -22,7 +21,6 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; -import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -50,11 +48,8 @@ import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.framework.util.JavaExpressionParseUtil; @@ -238,13 +233,8 @@ public void postAnalyze(ControlFlowGraph cfg) { * computation of AnnotatedTypeMirrors is completed, in particular in * addComputedTypeAnnotations(AnnotatedTypeMirror). * - *

That is, whether the given type is: - * - *

    - *
  1. An array type, whose component has non-empty MustCall type. - *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall - * type. - *
+ *

That is, whether the given type is a type assignable from java.util.Collection, whose only + * type var has non-empty MustCall type. * * @param t the AnnotatedTypeMirror * @return whether t is a resource collection @@ -335,13 +325,8 @@ public boolean isOwningCollectionParameter(Element elt) { /** * Returns whether the given AST tree is a resource collection. * - *

That is, whether the given tree is of: - * - *

    - *
  1. An array type, whose component has non-empty MustCall type. - *
  2. A type assignable from java.util.Collection, whose only type var has non-empty MustCall - * type. - *
+ *

That is, whether the given tree is of a type assignable from java.util.Collection, whose + * only type var has non-empty MustCall type. * * @param tree the tree * @return whether the tree is a resource collection @@ -358,14 +343,8 @@ public boolean isResourceCollection(Tree tree) { * If the given type is a collection, this method returns the MustCall values of its elements or * null if there are none or if the given type is not a collection. * - *

That is: - * - *

    - *
  1. if the given type is an array type, this method returns the MustCall values of its - * component type if there are any or else null. - *
  2. if the given type is a Java.util.Collection implementation, this method returns the - * MustCall values of its type variable upper bound if there are any or else null. - *
+ *

That is, if the given type is a Java.util.Collection implementation, this method returns the + * MustCall values of its type variable upper bound if there are any or else null. * * @param atm the AnnotatedTypeMirror * @return if the given type is a collection, returns the MustCall values of its elements or null @@ -376,12 +355,9 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType return null; } boolean isCollectionType = ResourceLeakUtils.isCollection(atm.getUnderlyingType()); - boolean isArrayType = atm.getKind() == TypeKind.ARRAY; AnnotatedTypeMirror componentType = null; - if (isArrayType) { - componentType = ((AnnotatedArrayType) atm).getComponentType(); - } else if (isCollectionType) { + if (isCollectionType) { List typeArgs = ((AnnotatedDeclaredType) atm).getTypeArguments(); if (typeArgs.size() != 0) { @@ -402,14 +378,8 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType * If the given tree represents a collection, this method returns the MustCall values of its * elements or null if there are none or if the given type is not a collection. * - *

That is: - * - *

    - *
  1. if the given tree is of an array type, this method returns the MustCall values of its - * component type if there are any or else null. - *
  2. if the given tree is of a Java.util.Collection implementation, this method returns the - * MustCall values of its type variable upper bound if there are any or else null. - *
+ *

That is, if the given tree is of a Java.util.Collection implementation, this method returns + * the MustCall values of its type variable upper bound if there are any or else null. * * @param tree the AST tree * @return if the given tree represents a collection, returns the MustCall values of its elements @@ -424,14 +394,8 @@ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { * If the given type is a collection, this method returns the MustCall values of its elements or * null if there are none or if the given type is not a collection. * - *

That is: - * - *

    - *
  1. if the given type is an array type, this method returns the MustCall values of its - * component type if there are any or else null. - *
  2. if the given type is a Java.util.Collection implementation, this method returns the - * MustCall values of its type variable upper bound if there are any or else null. - *
+ *

That is, if the given type is a Java.util.Collection implementation, this method returns the + * MustCall values of its type variable upper bound if there are any or else null. * * @param t the TypeMirror * @return if the given type is a collection, returns the MustCall values of its elements or null @@ -442,12 +406,9 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) return null; } boolean isCollectionType = ResourceLeakUtils.isCollection(t); - boolean isArrayType = t.getKind() == TypeKind.ARRAY; TypeMirror componentType = null; - if (isArrayType) { - componentType = ((ArrayType) t).getComponentType(); - } else if (isCollectionType) { + if (isCollectionType) { List typeArgs = ((DeclaredType) t).getTypeArguments(); if (typeArgs.size() != 0) { componentType = typeArgs.get(0); @@ -592,12 +553,6 @@ protected TypeAnnotator createTypeAnnotator() { super.createTypeAnnotator(), new CollectionOwnershipTypeAnnotator(this)); } - @Override - public TreeAnnotator createTreeAnnotator() { - return new ListTreeAnnotator( - super.createTreeAnnotator(), new CollectionOwnershipTreeAnnotator(this)); - } - /** * The TypeAnnotator for the Collection Ownership type system. * @@ -675,7 +630,7 @@ protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, b * Default resource collection fields to @OwningCollection and resource collection parameters to * @NotOwningCollection (inside the method). * - * Resource collections are either java.util.Collection's or arrays, whose component has + * Resource collections are either java.lang.Iterable's and java.util.Iterator's, whose component has * non-empty @MustCall type, as defined by the predicate isResourceCollection(AnnotatedTypeMirror). */ @Override @@ -702,31 +657,4 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { } } } - - /** - * The TreeAnnotator for the Collection Ownership type system. - * - *

This tree annotator treats newly allocated resource arrays (arrays, whose component type has - * non-empty MustCall value) as @OwningCollection. - */ - private class CollectionOwnershipTreeAnnotator extends TreeAnnotator { - - /** - * Create a CollectionOwnershipTreeAnnotator. - * - * @param collectionOwnershipAtf the type factory - */ - public CollectionOwnershipTreeAnnotator( - CollectionOwnershipAnnotatedTypeFactory collectionOwnershipAtf) { - super(collectionOwnershipAtf); - } - - @Override - public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { - if (isResourceCollection(type.getUnderlyingType())) { - type.replaceAnnotation(OWNINGCOLLECTIONWITHOUTOBLIGATION); - } - return super.visitNewArray(tree, type); - } - } } diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 7f2c539ba54f..1fe47ad90dc8 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.mustcall; import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.BlockTree; @@ -24,9 +23,7 @@ import com.sun.source.util.TreeScanner; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.lang.model.element.AnnotationMirror; @@ -360,19 +357,12 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { * the AST */ - /** Stores the size variable of the most recent array allocation per array name. */ - private final Map arrayInitializationSize = new HashMap<>(); - /** - * Checks through pattern-matching whether the loop either: + * Checks through pattern-matching whether the loop calls a method on entries of an + * {@code @OwningCollection}. * - *

    - *
  • initializes entries of an {@code @OwningCollection} - *
  • calls a method on entries of an {@code @OwningCollection} array - *
- * - * If yes, this is marked in some static datastructures in the - * {@code @MustCallOnElementsAnnotatedTypeFactory} + *

If yes, this is marked in some static datastructures in the + * {@code @CollectionOwnershipAnnotatedTypeFactory} */ @Override public Void visitForLoop(ForLoopTree tree, Void p) { @@ -384,8 +374,8 @@ public Void visitForLoop(ForLoopTree tree, Void p) { } /** - * Checks whether a for-loop potentially fulfills collection obligations of a collection/array and - * marks the loop in case the check is successful. + * Checks whether a for-loop potentially fulfills collection obligations of a collection and marks + * the loop in case the check is successful. * * @param tree forlooptree */ @@ -453,14 +443,13 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { } /** - * Decides for a for-loop header whether the loop iterates over all elements of some array based - * on a pattern-match with one-sided error with the following rules: + * Decides for a for-loop header whether the loop iterates over all elements of some collection + * based on a pattern-match with one-sided error with the following rules: * *

    *
  • only one loop variable *
  • initialization must be of the form i = 0 - *
  • condition must be of the form (i < arr.length) or (i < n), where n and arr are - * identifiers and n is effectively final + *
  • condition must be of the form (i < col.size()) *
  • update must be prefix or postfix *
* @@ -468,16 +457,14 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { * *
    *
  • null if any rule is violated - *
  • the name of the array if the loop condition is of the form (i < arr.length) - *
  • n if it is of the form (i < n), where n is an identifier. + *
  • the name of the collection if the loop condition is of the form (i < col.size()) *
* * @param init the initializer of the loop * @param condition the loop condition * @param update the loop update - * @return null if any rule is violated, or the name of the array if the loop condition is of the - * form {@code i < arr.length} or n if it is of the form {@code i < n}), where n is an - * identifier. + * @return null if any rule is violated, or the name of the collection if the loop condition is of + * the form (i < col.size()) */ protected Name verifyAllElementsAreCalledOn( StatementTree init, BinaryTree condition, ExpressionStatementTree update) { @@ -501,9 +488,7 @@ protected Name verifyAllElementsAreCalledOn( != ((IdentifierTree) inc.getExpression()).getName()) { // i=0 and i++ are same "i" return null; } - if (TreeUtils.isArrayLengthAccess(condition.getRightOperand())) { - return getNameFromExpressionTree(condition.getRightOperand()); - } else if ((condition.getRightOperand() instanceof MethodInvocationTree) + if ((condition.getRightOperand() instanceof MethodInvocationTree) && TreeUtils.isSizeAccess(condition.getRightOperand())) { ExpressionTree methodSelect = ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); @@ -514,8 +499,6 @@ protected Name verifyAllElementsAreCalledOn( return getNameFromExpressionTree(mst.getExpression()); } } - } else if (condition.getRightOperand() instanceof IdentifierTree) { - return getNameFromExpressionTree(condition.getRightOperand()); } } return null; @@ -599,18 +582,6 @@ identifierInHeader, getNameFromExpressionTree(mit))) { } return super.visitMethodInvocation(mit, p); } - - // check whether corresponds to arr[i] - @Override - public Void visitArrayAccess(ArrayAccessTree aat, Void p) { - boolean isIthArrayElement = getNameFromExpressionTree(aat.getIndex()) == iterator; - if (isIthArrayElement - && loopHeaderConsistentWithCollection( - identifierInHeader, getNameFromExpressionTree(aat))) { - collectionElementTree[0] = aat; - } - return super.visitArrayAccess(aat, p); - } }; for (StatementTree stmt : statements) { @@ -633,8 +604,6 @@ protected Name getNameFromExpressionTree(ExpressionTree expr) { switch (expr.getKind()) { case IDENTIFIER: return ((IdentifierTree) expr).getName(); - case ARRAY_ACCESS: - return getNameFromExpressionTree(((ArrayAccessTree) expr).getExpression()); case MEMBER_SELECT: Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); if (elt.getKind() == ElementKind.METHOD || elt.getKind() == ElementKind.FIELD) { @@ -668,7 +637,7 @@ protected Name getNameFromStatementTree(StatementTree expr) { } /** - * Returns the ExpressionTree of the collection/array in the given expression + * Returns the ExpressionTree of the collection in the given expression * * @param expr ExpressionTree * @return the expression evaluates to or null if it doesn't @@ -677,8 +646,6 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { switch (expr.getKind()) { case IDENTIFIER: return expr; - case ARRAY_ACCESS: - return ((ArrayAccessTree) expr).getExpression(); case MEMBER_SELECT: Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); if (elt.getKind() == ElementKind.METHOD) { @@ -697,8 +664,7 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { * Returns whether the given collection name is consistent with the identifier from the loop * header. * - *

That is, either the names are equal, or the identifier from the header is the same variable - * used to initialize the given collection. + *

That is, the names are equal. * *

Returns false if any argument is null. * @@ -710,9 +676,7 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collectionName) { if (idInHeader == null || collectionName == null) return false; boolean namesAreEqual = collectionName == idInHeader; - Name initSize = arrayInitializationSize.get(collectionName); - boolean idInHeaderIsSizeOfCollection = initSize != null && initSize == idInHeader; - return namesAreEqual || idInHeaderIsSizeOfCollection; + return namesAreEqual; } /** diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 6e348a5d5762..1d156f0870c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -67,11 +67,9 @@ import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; -import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.ClassNameNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; -import org.checkerframework.dataflow.cfg.node.LessThanNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -80,7 +78,6 @@ import org.checkerframework.dataflow.cfg.node.ReturnNode; import org.checkerframework.dataflow.cfg.node.SuperNode; import org.checkerframework.dataflow.cfg.node.ThisNode; -import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode; import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.IteratedCollectionElement; import org.checkerframework.dataflow.expression.JavaExpression; @@ -3284,8 +3281,6 @@ public void findFulfillingForEachLoops(ControlFlowGraph cfg) { for (Node node : currentBlock.getNodes()) { if (node instanceof MethodInvocationNode) { patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg); - } else if (node instanceof ArrayAccessNode) { - patternMatchEnhancedArrayForLoop((ArrayAccessNode) node, cfg); } } propagate( @@ -3414,99 +3409,6 @@ private void patternMatchEnhancedCollectionForLoop( } } - /** - * Checks whether the given {@code ArrayAccessNode} is desugared from an enhanced for loop and - * calls a loop-body-analysis on the detected loop if it is. - * - *

If an {@code ArrayAccessNode} is desugared from an enhanced for loop over an array, it - * corresponds to the node in the synthetic {@code s = array#numX[index#numY]} assignment, where - * the loop iterator variable is assigned. The AST node corresponding to the loop itself is in - * this case contained as a field in the {@code ArrayAccessNode}, which is set in the CFG - * translation phase one. - * - *

This method now traverses the CFG upwards to find the loop condition and downwards to find - * the first block of the loop body. With these two blocks, it can then call a loop-body-analysis - * to find the methods the loop calls on the elements of the iterated collection, as part of the - * MustCallOnElements checker. - * - * @param arrayAccessNode the {@code ArrayAccessNode}, for which it is checked, whether it is - * desugared from an enhanced for loop. - * @param cfg the enclosing cfg of the {@code ArrayAccessNode} - */ - private void patternMatchEnhancedArrayForLoop( - ArrayAccessNode arrayAccessNode, ControlFlowGraph cfg) { - boolean nodeIsDesugaredFromEnhancedForLoop = arrayAccessNode.getArrayExpression() != null; - if (nodeIsDesugaredFromEnhancedForLoop && cfg != null) { - // this is the arr[i] access desugared from an enhanced-for-loop (in iter = arr[i];) - EnhancedForLoopTree loop = arrayAccessNode.getEnhancedForLoop(); - if (loop == null) { - throw new BugInCF( - "MethodInvocationNode.iterableExpression should be non-null iff" - + " MethodInvocationNode.enhancedForLoop is non-null"); - } - - // Find the first block of the loop body. - SingleSuccessorBlock ssblock = (SingleSuccessorBlock) arrayAccessNode.getBlock(); - Block loopBodyEntryBlock = ssblock.getSuccessor(); - - // Find the loop condition - // Start from the synthetic (desugared) arr[i] node and traverse the cfg - // backwards until the LessThan node is found. - // It corresponds to the desugared loop condition (index#numX < array#numX.length). - Block block = arrayAccessNode.getBlock(); - Iterator nodeIterator = block.getNodes().iterator(); - Node loopVarNode = null; - Node node; - do { - while (!nodeIterator.hasNext()) { - Set predBlocks = block.getPredecessors(); - if (predBlocks.size() == 1) { - block = predBlocks.iterator().next(); - nodeIterator = block.getNodes().iterator(); - } else { - throw new BugInCF( - "Encountered more than one CFG Block predeccessor trying to find the" - + " enhanced-for-loop update block."); - } - } - node = nodeIterator.next(); - if (node instanceof VariableDeclarationNode) { - // variable declaration of public iterator - loopVarNode = node; - } - } while (!(node instanceof LessThanNode)); - - Block blockContainingLoopCondition = node.getBlock(); - if (blockContainingLoopCondition.getSuccessors().size() != 1) { - throw new BugInCF( - "loop condition has: " - + blockContainingLoopCondition.getSuccessors().size() - + " successors instead of 1."); - } - Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); - if (!(conditionalBlock instanceof ConditionalBlock)) { - throw new BugInCF( - "loop condition successor is not ConditionalBlock, but: " - + conditionalBlock.getClass()); - } - - // add the blocks into a static datastructure in the calledmethodsatf, such that it can - // analyze - // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees - // to the static datastructure in McoeAtf) - PotentiallyFulfillingLoop pfLoop = - new PotentiallyFulfillingLoop( - loop.getExpression(), - loopVarNode.getTree(), - node.getTree(), - loopBodyEntryBlock, - block, - (ConditionalBlock) conditionalBlock, - loopVarNode); - this.analyzeObligationFulfillingLoop(cfg, pfLoop); - } - } - /** * Analyze the loop body of a 'potentially-mcoe-obligation-fulfilling-loop', as determined by a * pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or determined by a diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 03662101463d..114c7887b4e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -2,7 +2,6 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -611,13 +610,8 @@ public TransferInput getInput(Block block) /** Wrapper class for a loop that might have an effect on the obligation of a collection/array. */ public abstract static class CollectionObligationAlteringLoop { - /** Loop is either assigning or fulfilling. */ + /** Loop is fulfilling. */ public static enum LoopKind { - /** - * Loop potentially assigns elements with non-empty {@code @MustCall} type to a collection. - */ - ASSIGNING, - /** Loop potentially calls methods on all elements of a collection. */ FULFILLING } @@ -695,40 +689,6 @@ public Set getMethods() { } } - /** - * Wrapper for a loop that potentially assigns elements with non-empty {@code MustCall} - * obligations to an array, thus creating {@code MustCallOnElements} obligations for the array. - */ - public static class PotentiallyAssigningLoop extends CollectionObligationAlteringLoop { - /** The AST tree for the assignment of the resource into the array in the loop. */ - public final AssignmentTree assignment; - - /** - * Constructs a new {@code PotentiallyAssigningLoop} - * - * @param collectionTree AST {@code Tree} for collection iterated over - * @param collectionElementTree AST {@code Tree} for collection element iterated over - * @param condition AST {@code Tree} for loop condition - * @param assignment AST tree for the assignment of the resource into the array in the loop - * @param methodsToCall set of methods that are to be added to the {@code MustCallOnElements} - * type of the array iterated over. - */ - public PotentiallyAssigningLoop( - ExpressionTree collectionTree, - ExpressionTree collectionElementTree, - Tree condition, - AssignmentTree assignment, - Set methodsToCall) { - super( - collectionTree, - collectionElementTree, - condition, - Set.copyOf(methodsToCall), - CollectionObligationAlteringLoop.LoopKind.ASSIGNING); - this.assignment = assignment; - } - } - /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ public static class PotentiallyFulfillingLoop extends CollectionObligationAlteringLoop { From 5f32f8936da22e038cfe9b12d21bb5c504499b32 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 15:40:46 +0200 Subject: [PATCH 220/374] fix test outcomse after making List#get not owning --- checker/tests/resourceleak/SocketIntoList.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index 6a34c8c1b45a..a35c397e7878 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -37,8 +37,11 @@ public void test3(List<@MustCall({}) Socket> l) throws Exception { // This input list might have been produced by e.g., test1() public void test4(List<@MustCall({}) Socket> l) throws Exception { - // :: error: required.method.not.called + // l.get(0) is not an error as List#get returns @NotOwning. + // However, s.bind tries to reset the mustcall obligations of s, + // which is only permitted if s is owning. Socket s = l.get(0); + // :: error: reset.not.owning s.bind(new InetSocketAddress("192.168.0.1", 0)); } From ab5b2a2f56ff2ba92f6692cbbe954f173cdc47b9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 6 Jul 2025 15:40:57 +0200 Subject: [PATCH 221/374] remove array tests --- .../CollectionOwnershipDefaults.java | 20 ---- .../LoopBodyAnalysisTest.java | 103 ++++++------------ 2 files changed, 35 insertions(+), 88 deletions(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index c0367ca0e363..9aa11d255414 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -105,20 +105,6 @@ void checkNewResourceCollectionDefault() { checkArgIsOwning(newResourceCollection); // :: error: argument checkArgIsOCwoO(newResourceCollection); - - Socket[] newResourceArray = new Socket[n]; - checkArgIsOwning(newResourceArray); - // :: error: argument - checkArgIsOCwoO(newResourceArray); - } - - void closeElements(Socket @OwningCollection [] socketCollection) { - for (Socket s : socketCollection) { - try { - s.close(); - } catch (Exception e) { - } - } } void closeElements(@OwningCollection Collection socketCollection) { @@ -134,14 +120,8 @@ void checkArgIsOwning( // :: error: unfulfilled.collection.obligations @OwningCollection Collection collection) {} - // :: error: unfulfilled.collection.obligations - void checkArgIsOwning(Socket @OwningCollection [] collection) {} - void checkArgIsOCwoO( // :: error: illegal.type.annotation @OwningCollectionWithoutObligation Collection collection) {} - - // :: error: illegal.type.annotation - void checkArgIsOCwoO(Socket @OwningCollectionWithoutObligation [] collection) {} } diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index ae2b399fb223..5890c9930ece 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -15,14 +15,6 @@ void fullSatisfyCollection(@OwningCollection Collection resources) { checkArgIsOCWO(resources); } - void fullSatisfyArray(Resource @OwningCollection [] resources) { - for (Resource r : resources) { - r.close(); - r.flush(); - } - checkArgIsOCWO(resources); - } - // here, the argument defaults to @NotOwningCollection. // the loop should not change that type void fullSatisfyCollectionNotOwning(Collection resources) { @@ -34,26 +26,6 @@ void fullSatisfyCollectionNotOwning(Collection resources) { checkArgIsOCWO(resources); } - // here, the argument defaults to @NotOwningCollection. - // the loop should not change that type - void fullSatisfyArrayNotOwning(Resource[] resources) { - for (Resource r : resources) { - r.close(); - r.flush(); - } - // :: error: argument - checkArgIsOCWO(resources); - } - - // :: error: unfulfilled.collection.obligations - void partialSatisfyArrayShouldError(Resource @OwningCollection [] resources) { - for (Resource r : resources) { - r.close(); - } - // :: error: argument - checkArgIsOCWO(resources); - } - // :: error: unfulfilled.collection.obligations void partialSatisfyCollectionShouldError(@OwningCollection Collection resources) { for (Resource r : resources) { @@ -84,7 +56,7 @@ void multipleMustCallFull() { checkArgIsOCWO(l); } - void tryCatchShouldWork(Resource @OwningCollection [] resources) { + void tryCatchShouldWork(@OwningCollection List resources) { for (Resource r : resources) { try { r.close(); @@ -94,14 +66,14 @@ void tryCatchShouldWork(Resource @OwningCollection [] resources) { } } - void methodCallInsideLoop(Resource @OwningCollection [] resources) { + void methodCallInsideLoop(@OwningCollection List resources) { for (Resource r : resources) { doCloseFlush(r); } } // :: error: unfulfilled.collection.obligations - void earlyBreak(Resource @OwningCollection [] resources) { + void earlyBreak(@OwningCollection List resources) { for (Resource r : resources) { r.close(); r.flush(); @@ -109,33 +81,31 @@ void earlyBreak(Resource @OwningCollection [] resources) { } } - void tryWithResources(Resource @OwningCollection [] resources) { - for (Resource r : resources) { - try { - try (Resource auto = r) { - auto.flush(); - } - } catch (Exception e) { - } - } - } - - void nullableElementWithCheck(Resource @OwningCollection [] resources) { - for (Resource r : resources) { - if (r != null) { - r.close(); - r.flush(); - } - } - } - - void nullableElementHelper(Resource @OwningCollection [] resources) { - for (Resource r : resources) { - if (r != null) { - doCloseFlush(r); - } - } - } + // TODO SCK: uncomment these tests + // void tryWithResources(@OwningCollection List resources) { + // for (Resource r : resources) { + // try (Resource auto = r) { + // auto.flush(); + // } + // } + // } + + // void nullableElementWithCheck(@OwningCollection List resources) { + // for (Resource r : resources) { + // if (r != null) { + // r.close(); + // r.flush(); + // } + // } + // } + + // void nullableElementHelper(@OwningCollection List resources) { + // for (Resource r : resources) { + // if (r != null) { + // doCloseFlush(r); + // } + // } + // } @EnsuresCalledMethods( value = "#1", @@ -145,17 +115,17 @@ void doCloseFlush(Resource r) { r.flush(); } - void indexForLoop(Resource @OwningCollection [] resources) { - for (int i = 0; i < resources.length; i++) { - resources[i].close(); - resources[i].flush(); + void indexForLoop(@OwningCollection List resources) { + for (int i = 0; i < resources.size(); i++) { + resources.get(i).close(); + resources.get(i).flush(); } } // :: error: unfulfilled.collection.obligations - void indexForLoopPartial(Resource @OwningCollection [] resources) { - for (int i = 0; i < resources.length; i++) { - resources[i].close(); + void indexForLoopPartial(@OwningCollection List resources) { + for (int i = 0; i < resources.size(); i++) { + resources.get(i).close(); // missing flush } } @@ -170,7 +140,4 @@ void indexForLoopList(@OwningCollection List resources) { // :: error: illegal.type.annotation void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} - - // :: error: illegal.type.annotation - void checkArgIsOCWO(Resource @OwningCollectionWithoutObligation [] arg) {} } From 9e3c23a67851ec5c1346a3e0473fd95427020702 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 7 Jul 2025 16:04:29 +0200 Subject: [PATCH 222/374] fix collections rlc manual section with manu feedback --- docs/manual/resource-leak-checker.tex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index efc20d31f372..a8701a67a2f8 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -646,18 +646,17 @@ \sectionAndLabel{Collections of resources}{resource-leak-collections} The Resource Leak Checker handles homogeneous collections of resources. In a homogeneous collection, every element -has exactly the same must-call and called-methods properties. \texttt{java.util.Iterable} and all its implementations, and \texttt{java.util.Iterator}s are supported; this section -calls those ``collections''. +has exactly the same must-call and called-methods properties. Instances of \texttt{java.util.Iterable} are supported this section calls those ``collections''. Usage of \texttt{java.util.Iterator}s over \texttt{java.util.Iterable}s are also supported, but they are not considered collections. -The Resource Leak Checker tracks every allocated resource collection in two ways: +What the checker effectively verifies is that any resource collection is properly disposed of. That is, there is some loop that definitely calls the required methods on all elements of the collection. + +Towards that end, the Resource Leak Checker tracks every allocated resource collection in two ways: \begin{itemize} \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - they can't be used to add or remove elements to and from the collection respectively for example. - \item a complementary dataflow analysis tracks an obligation for each allocated resource collection. The obligation can be passed on to method parameters, fields, or return values for example, just like the ownership. The difference is the imprecision - the type system tracks at most one owner per resource collection, while the dataflow analysis tracks at least one obligation per resource collection to ensure it is definitely fulfilled. + \item a complementary dataflow analysis tracks an obligation for each allocated resource collection. The obligation can be passed on to method parameters, fields, or return values for example, just like the ownership. The difference is that while the type system tracks at most one owner per resource collection allowed to mutate it without restriction, the dataflow analysis tracks at least one obligation per resource collection to ensure it is definitely fulfilled. \end{itemize} -What the checker effectively verifies is that any resource collection is properly disposed of. That is, there is some loop that definitely calls the required methods on all elements of the collection. - For the purposes of this checker, \textit{resource collections} are precisely defined, as the objects of interest for both the type system and obligation tracking. A Java expression is a \textit{resource collection} if it is: \begin{enumerate} @@ -665,7 +664,7 @@ \item Its type parameter upper bound has non-empty \texttt{MustCall} type. \end{enumerate} -For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collection, but one of type \texttt{Set} is not. +For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collection, but one of type \texttt{Set} is not. \subsectionAndLabel{Ownership type system for resource collections}{resource-leak-collections-ownership-types} Of the two tracking mechanisms described above, the ownership type system is more visible from the user perspective. The type hierarchy is the following: From bbb7ef8c0c16cf989d7f1931242a384bac5ed1c9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 7 Jul 2025 16:05:11 +0200 Subject: [PATCH 223/374] also require createsmcfor(this) for field asgnment with noc rhs --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 1d156f0870c1..ac5bf4200eff 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1910,7 +1910,8 @@ private void checkReassignmentToOwningCollectionField( case OwningCollectionWithoutObligation: // no obligation. assignment allowed. // but if rhs is owning, demand CreatesMustCallFor("this") - if (rhsCoType == CollectionOwnershipType.OwningCollection) { + if (rhsCoType == CollectionOwnershipType.OwningCollection + || rhsCoType == CollectionOwnershipType.NotOwningCollection) { checkEnclosingMethodIsCreatesMustCallFor(lhs, enclosingMethodTree); if (isOwningCollectionField) { Set obligationsForVar = getObligationsForVar(obligations, rhs.getTree()); From 62c4299ef6eb82027c685c5844dcbe3e7615f200 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 9 Jul 2025 10:13:19 +0200 Subject: [PATCH 224/374] maps are now also resource collections --- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 +- .../checker/mustcall/MustCallAnnotatedTypeFactory.java | 1 - .../checker/resourceleak/ResourceLeakUtils.java | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 6507df83baf8..9db67e89581a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -411,7 +411,7 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) if (isCollectionType) { List typeArgs = ((DeclaredType) t).getTypeArguments(); if (typeArgs.size() != 0) { - componentType = typeArgs.get(0); + componentType = typeArgs.get(typeArgs.size() - 1); } } diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 9e1984717ad5..57d626a43827 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -175,7 +175,6 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { */ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclaredType adt) { if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) { - // || ResourceLeakUtils.isIterator(adt.getUnderlyingType())) { for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) { if (typeArg == null) continue; if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 12ce07302486..b14e56ea431c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -289,7 +290,8 @@ public static boolean isCollection(TypeMirror type) { Class elementRawType = TypesUtils.getClassFromType(type); if (elementRawType == null) return false; return Iterable.class.isAssignableFrom(elementRawType) - || Iterator.class.isAssignableFrom(elementRawType); + || Iterator.class.isAssignableFrom(elementRawType) + || Map.class.isAssignableFrom(elementRawType); } /** From 66901eec63856c4a25e407beed0f198941fc02b3 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 9 Jul 2025 10:13:56 +0200 Subject: [PATCH 225/374] remove expected warning for type.arg because map can now hold non-empty MC values --- checker/tests/resourceleak/IndexMode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak/IndexMode.java b/checker/tests/resourceleak/IndexMode.java index b41a251310db..10b6d55c5f7f 100644 --- a/checker/tests/resourceleak/IndexMode.java +++ b/checker/tests/resourceleak/IndexMode.java @@ -69,8 +69,8 @@ public static void getMode5(Map indexOptions) { } // This variant uses an InputStream (which has a MustCall type by default) as the - // value type in the map. - // :: error: type.argument + // value type in the map. This is not an error anymore, as the values are permitted + // to have any @MustCall type. public static Object getModeIS(Map indexOptions) { try { // :: error: required.method.not.called From be87c39263ae80156a3f72b32b1c2beb0e6cece2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 9 Jul 2025 14:30:36 +0200 Subject: [PATCH 226/374] @OwningCollection and @NotOwningCollection are inheritable!!!! --- .../checker/collectionownership/qual/NotOwningCollection.java | 2 ++ .../checker/collectionownership/qual/OwningCollection.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java index 8aca48f9912d..15cc8c70339c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; import org.checkerframework.framework.qual.SubtypeOf; /** @@ -15,6 +16,7 @@ *

This annotation can be enforced by running the Resource Leak Checker. It enforces that the * expression is not used to add to or remove elements from the underlying collection/array. */ +@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java index cb02b5f786e1..f2b08aba03ec 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.InheritedAnnotation; /** * An expression of type {@code @OwningCollection} is a resource collection/array, which definitely @@ -18,6 +19,7 @@ * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). */ +@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({NotOwningCollection.class}) From 79506bd349ac421b2e18af7bc7dd913045813a3d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 9 Jul 2025 17:33:07 +0200 Subject: [PATCH 227/374] polyowning inherited --- .../checker/collectionownership/qual/PolyOwningCollection.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java index 402419340f5c..4107c8570aaa 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java @@ -5,6 +5,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; import org.checkerframework.framework.qual.PolymorphicQualifier; /** @@ -13,6 +14,7 @@ * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @Documented +@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @PolymorphicQualifier(NotOwningCollection.class) From 71ae840c691ca25e4fef93e31f44a145d4e28837 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 9 Jul 2025 17:33:24 +0200 Subject: [PATCH 228/374] only collections with one typearg are rc --- .../CollectionOwnershipAnnotatedTypeFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 9db67e89581a..b648d0a68bfb 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -360,7 +360,7 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType if (isCollectionType) { List typeArgs = ((AnnotatedDeclaredType) atm).getTypeArguments(); - if (typeArgs.size() != 0) { + if (typeArgs.size() == 1) { componentType = typeArgs.get(0); } } @@ -410,8 +410,8 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) TypeMirror componentType = null; if (isCollectionType) { List typeArgs = ((DeclaredType) t).getTypeArguments(); - if (typeArgs.size() != 0) { - componentType = typeArgs.get(typeArgs.size() - 1); + if (typeArgs.size() == 1) { + componentType = typeArgs.get(0); } } From b1687a6007992e545801ce675599ede50789c525 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 14 Jul 2025 13:22:42 +0200 Subject: [PATCH 229/374] fix crash when annotated type not available --- .../CollectionOwnershipAnnotatedTypeFactory.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index b648d0a68bfb..ee7f2f924475 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -56,6 +56,7 @@ import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; @@ -334,7 +335,12 @@ public boolean isOwningCollectionParameter(Element elt) { public boolean isResourceCollection(Tree tree) { if (tree == null) return false; MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); - AnnotatedTypeMirror treeMcType = mcAtf.getAnnotatedType(tree); + AnnotatedTypeMirror treeMcType = null; + try { + treeMcType = mcAtf.getAnnotatedType(tree); + } catch (BugInCF e) { + return false; + } List list = getMustCallValuesOfResourceCollectionComponent(treeMcType); return list != null && list.size() > 0; } From 2b13a8dd44f44e007651d86253a5a8c96b51bda6 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 21 Jul 2025 09:19:46 -0700 Subject: [PATCH 230/374] Format --- .../checker/collectionownership/qual/OwningCollection.java | 2 +- .../CollectionOwnershipAnnotatedTypeFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java index f2b08aba03ec..a7bdfdde870f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -4,8 +4,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.SubtypeOf; /** * An expression of type {@code @OwningCollection} is a resource collection/array, which definitely diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ee7f2f924475..62b00b1666ea 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -339,7 +339,7 @@ public boolean isResourceCollection(Tree tree) { try { treeMcType = mcAtf.getAnnotatedType(tree); } catch (BugInCF e) { - return false; + return false; } List list = getMustCallValuesOfResourceCollectionComponent(treeMcType); return list != null && list.size() > 0; From 518e749fbf9d96720440b88063a6e757252b9e5b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 21 Jul 2025 22:57:47 +0200 Subject: [PATCH 231/374] fix loopbodyanalysis (fix by Suzanne) --- .../MustCallConsistencyAnalyzer.java | 51 +++++++++++++------ .../RLCCalledMethodsAnnotatedTypeFactory.java | 18 +++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index ac5bf4200eff..f224600c60ee 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3494,14 +3494,16 @@ public void analyzeObligationFulfillingLoop( boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock; if (isLastBlockOfBody) { Set calledMethodsAfterBlock = - analyzeTypeOfCollectionElement(currentBlock, potentiallyFulfillingLoop, obligations); + analyzeTypeOfCollectionElement(currentBlock, potentiallyFulfillingLoop, obligations, loopUpdateBlock); // intersect the called methods after this block with the accumulated ones so far. // This is required because there may be multiple "back edges" of the loop, in which // case we must intersect the called methods between those. - if (calledMethodsInLoop == null) { - calledMethodsInLoop = calledMethodsAfterBlock; - } else { - calledMethodsInLoop.retainAll(calledMethodsAfterBlock); + if (calledMethodsAfterBlock != null) { + if (calledMethodsInLoop == null) { + calledMethodsInLoop = calledMethodsAfterBlock; + } else { + calledMethodsInLoop.retainAll(calledMethodsAfterBlock); + } } } else { try { @@ -3535,15 +3537,22 @@ public void analyzeObligationFulfillingLoop( * @param lastLoopBodyBlock last block of loop body * @param potentiallyFulfillingLoop loop wrapper of the loop to analyze * @param obligations the set of tracked obligations + * @param loopUpdateBlock block that updates the loop * @return the union of methods in the CalledMethods type of the collection element and all its - * resource aliases. + * resource aliases or {@code null} if the called methods is bottom */ private Set analyzeTypeOfCollectionElement( Block lastLoopBodyBlock, PotentiallyFulfillingLoop potentiallyFulfillingLoop, - Set obligations) { + Set obligations, + Block loopUpdateBlock) { AccumulationStore store = null; - if (lastLoopBodyBlock.getLastNode() == null) { + if (lastLoopBodyBlock.getType() == BlockType.CONDITIONAL_BLOCK) { + ConditionalBlock conditionalBlock = (ConditionalBlock) lastLoopBodyBlock; + boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock; + store = cmAtf.getStoreAfterConditionalBlock(conditionalBlock, thenSuccessor); + } else if (lastLoopBodyBlock.getLastNode() == null) { + // TODO SCK: this probably can't happen anymore // TODO is this really the right store? I think we need to get the then-or else store store = cmAtf.getStoreAfterBlock(lastLoopBodyBlock); } else { @@ -3561,7 +3570,7 @@ private Set analyzeTypeOfCollectionElement( // + potentiallyFulfillingLoop.collectionElementTree); } - Set calledMethodsAfterThisBlock = new HashSet<>(); + Set calledMethodsAfterThisBlock = null; // add the called methods of the ICE IteratedCollectionElement ice = @@ -3571,18 +3580,24 @@ private Set analyzeTypeOfCollectionElement( if (ice != null) { AccumulationValue cmValOfIce = store.getValue(ice); List calledMethods = getCalledMethods(cmValOfIce); - if (calledMethods != null && calledMethods.size() > 0) { - calledMethodsAfterThisBlock.addAll(calledMethods); + if (calledMethods != null) { + calledMethodsAfterThisBlock = new HashSet<>(calledMethods); } } // add the called methods of possible aliases of the collection element for (ResourceAlias alias : collectionElementObligation.resourceAliases) { AccumulationValue cmValOfAlias = store.getValue(alias.reference); - if (cmValOfAlias == null) continue; + if (cmValOfAlias == null) { + continue; + } List calledMethods = getCalledMethods(cmValOfAlias); - if (calledMethods != null && calledMethods.size() > 0) { - calledMethodsAfterThisBlock.addAll(calledMethods); + if (calledMethods != null) { + if (calledMethodsAfterThisBlock == null) { + calledMethodsAfterThisBlock = new HashSet<>(calledMethods); + } else { + calledMethodsAfterThisBlock.addAll(calledMethods); + } } } @@ -3590,10 +3605,11 @@ private Set analyzeTypeOfCollectionElement( } /** - * Returns the set of called methods values given an AccumulationValue. + * Returns the set of called methods values given an AccumulationValue or null if the accumulation + * value is bottom. * * @param cmVal the accumulation value - * @return the set of called methods of the given value + * @return the set of called methods of the given value or null if the accumulation value is bottom */ private List getCalledMethods(AccumulationValue cmVal) { Set calledMethods = cmVal.getAccumulatedValues(); @@ -3604,6 +3620,9 @@ private List getCalledMethods(AccumulationValue cmVal) { if (AnnotationUtils.areSameByName( anno, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { return cmAtf.getCalledMethods(anno); + } else if (AnnotationUtils.areSameByName( + anno, "org.checkerframework.checker.calledmethods.qual.CalledMethodsBottom")) { + return null; } } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 114c7887b4e2..9440da1fc0d8 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -437,6 +437,24 @@ public AccumulationStore getStoreAfterBlock(Block block) { return flowResult.getStoreAfter(block); } + /** + * Returns the then or else store after {@code block} depending on the value of {@code then} is + * returned. + * + * @param block a conditional block + * @param then wether the then store should be returned + * @return the then or else store after {@code block} depending on the value of {@code then} is + * returned + */ + public AccumulationStore getStoreAfterConditionalBlock(ConditionalBlock block, boolean then) { + TransferInput transferInput = flowResult.getInput(block); + assert transferInput != null : "@AssumeAssertion(nullness): transferInput should be non-null"; + if (then) { + return transferInput.getThenStore(); + } + return transferInput.getElseStore(); + } + @Override @SuppressWarnings("TypeParameterUnusedInFormals") // Intentional abuse public > From fe206ee9741b306697901249c9796170a6353edf Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 21 Jul 2025 22:58:08 +0200 Subject: [PATCH 232/374] no errors for map.get --- checker/tests/resourceleak/IndexMode.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/tests/resourceleak/IndexMode.java b/checker/tests/resourceleak/IndexMode.java index 10b6d55c5f7f..522815964139 100644 --- a/checker/tests/resourceleak/IndexMode.java +++ b/checker/tests/resourceleak/IndexMode.java @@ -38,9 +38,9 @@ public static Object getMode2(Map indexOpt // This copy of getMode() adds an explicit `@MustCall` annotation to the String and to // the local variable. This version currently works as expected, unlike getMode2(). + // Since Map#get returns @NotOwning, this reports no error. public static Object getMode2a(Map indexOptions) { try { - // :: error: required.method.not.called @MustCall("hashCode") String literalOption = indexOptions.get("is_literal"); } catch (Exception e) { } @@ -51,7 +51,7 @@ public static Object getMode2a(Map indexOp // This copy of getMode() adds an explicit `@MustCall` annotation to the String and removes // the try-catch. public static Object getMode3(Map indexOptions) { - // :: error: required.method.not.called + // Since Map#get returns @NotOwning, this reports no error. String literalOption = indexOptions.get("is_literal"); return null; } @@ -59,7 +59,7 @@ public static Object getMode3(Map indexOpt // This copy of getMode() adds an explicit `@MustCall` annotation to the String, removes // the try-catch, and makes the return type void. public static void getMode4(Map indexOptions) { - // :: error: required.method.not.called + // Since Map#get returns @NotOwning, this reports no error. String literalOption = indexOptions.get("is_literal"); } @@ -73,7 +73,7 @@ public static void getMode5(Map indexOptions) { // to have any @MustCall type. public static Object getModeIS(Map indexOptions) { try { - // :: error: required.method.not.called + // Since Map#get returns @NotOwning, this reports no error. InputStream literalOption = indexOptions.get("is_literal"); } catch (Exception e) { } From 4c3dc9c606ab222b820aea4f09419c4eada50612 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 21 Jul 2025 22:58:20 +0200 Subject: [PATCH 233/374] uncomment now working loop body analysis tests --- .../LoopBodyAnalysisTest.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 5890c9930ece..077ce0d18fe4 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -90,22 +90,22 @@ void earlyBreak(@OwningCollection List resources) { // } // } - // void nullableElementWithCheck(@OwningCollection List resources) { - // for (Resource r : resources) { - // if (r != null) { - // r.close(); - // r.flush(); - // } - // } - // } + void nullableElementWithCheck(@OwningCollection List resources) { + for (Resource r : resources) { + if (r != null) { + r.close(); + r.flush(); + } + } + } - // void nullableElementHelper(@OwningCollection List resources) { - // for (Resource r : resources) { - // if (r != null) { - // doCloseFlush(r); - // } - // } - // } + void nullableElementHelper(@OwningCollection List resources) { + for (Resource r : resources) { + if (r != null) { + doCloseFlush(r); + } + } + } @EnsuresCalledMethods( value = "#1", From 82717fc5bfaad975893aa3fc738f8c8a111bb82b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Mon, 21 Jul 2025 22:59:10 +0200 Subject: [PATCH 234/374] formatting --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index f224600c60ee..521bd9eabf4a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3494,11 +3494,12 @@ public void analyzeObligationFulfillingLoop( boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock; if (isLastBlockOfBody) { Set calledMethodsAfterBlock = - analyzeTypeOfCollectionElement(currentBlock, potentiallyFulfillingLoop, obligations, loopUpdateBlock); + analyzeTypeOfCollectionElement( + currentBlock, potentiallyFulfillingLoop, obligations, loopUpdateBlock); // intersect the called methods after this block with the accumulated ones so far. // This is required because there may be multiple "back edges" of the loop, in which // case we must intersect the called methods between those. - if (calledMethodsAfterBlock != null) { + if (calledMethodsAfterBlock != null) { if (calledMethodsInLoop == null) { calledMethodsInLoop = calledMethodsAfterBlock; } else { @@ -3609,7 +3610,8 @@ private Set analyzeTypeOfCollectionElement( * value is bottom. * * @param cmVal the accumulation value - * @return the set of called methods of the given value or null if the accumulation value is bottom + * @return the set of called methods of the given value or null if the accumulation value is + * bottom */ private List getCalledMethods(AccumulationValue cmVal) { Set calledMethods = cmVal.getAccumulatedValues(); From f474bc53a20da31860e2b4ea30543bde5411ff0d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 22 Jul 2025 12:41:24 +0200 Subject: [PATCH 235/374] suppress interning warning --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 521bd9eabf4a..8294c67f29e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3550,12 +3550,12 @@ private Set analyzeTypeOfCollectionElement( AccumulationStore store = null; if (lastLoopBodyBlock.getType() == BlockType.CONDITIONAL_BLOCK) { ConditionalBlock conditionalBlock = (ConditionalBlock) lastLoopBodyBlock; + @SuppressWarnings("interning:not.interned") boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock; store = cmAtf.getStoreAfterConditionalBlock(conditionalBlock, thenSuccessor); } else if (lastLoopBodyBlock.getLastNode() == null) { - // TODO SCK: this probably can't happen anymore - // TODO is this really the right store? I think we need to get the then-or else store - store = cmAtf.getStoreAfterBlock(lastLoopBodyBlock); + throw new BugInCF("Loop Body Analysis -- Block " + lastLoopBodyBlock + " has no nodes"); + // store = cmAtf.getStoreAfterBlock(lastLoopBodyBlock); } else { store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); } From 398d16c64beab05bae4ded21d01c5be8919d0433 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 22 Jul 2025 13:53:39 +0200 Subject: [PATCH 236/374] fix crash in for-loop pattern match --- .../org/checkerframework/checker/mustcall/MustCallVisitor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 1fe47ad90dc8..47c87889fa81 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -390,6 +390,9 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { StatementTree init = tree.getInitializer().get(0); ExpressionTree condition = tree.getCondition(); ExpressionStatementTree update = tree.getUpdate().get(0); + if (!(condition instanceof BinaryTree)) { + return; + } Name identifierInHeader = verifyAllElementsAreCalledOn(init, (BinaryTree) condition, update); Name iterator = getNameFromStatementTree(init); if (identifierInHeader == null || iterator == null) { From f30abbbe3d53b2a79887f855e732b1fd3f2cc67a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 22 Jul 2025 18:41:44 +0200 Subject: [PATCH 237/374] make collectionownership annotations inheritable --- .../qual/NotOwningCollection.java | 2 - .../qual/OwningCollection.java | 2 - .../qual/PolyOwningCollection.java | 2 - ...llectionOwnershipAnnotatedTypeFactory.java | 68 ++++++++++++++++++- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java index 15cc8c70339c..8aca48f9912d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -4,7 +4,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; import org.checkerframework.framework.qual.SubtypeOf; /** @@ -16,7 +15,6 @@ *

This annotation can be enforced by running the Resource Leak Checker. It enforces that the * expression is not used to add to or remove elements from the underlying collection/array. */ -@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java index a7bdfdde870f..cb02b5f786e1 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -4,7 +4,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; import org.checkerframework.framework.qual.SubtypeOf; /** @@ -19,7 +18,6 @@ * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). */ -@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({NotOwningCollection.class}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java index 4107c8570aaa..402419340f5c 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java @@ -5,7 +5,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; import org.checkerframework.framework.qual.PolymorphicQualifier; /** @@ -14,7 +13,6 @@ * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @Documented -@InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @PolymorphicQualifier(NotOwningCollection.class) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 62b00b1666ea..fa3c20ba861c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -49,9 +49,11 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.framework.util.JavaExpressionParseUtil; import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; @@ -577,8 +579,72 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { } @Override - public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { + public Void visitExecutable(AnnotatedExecutableType t, Void p) { + List params = t.getParameterTypes(); + + AnnotatedDeclaredType receiver = t.getReceiverType(); + AnnotationMirror receiverAnno = + receiver == null ? null : receiver.getEffectiveAnnotationInHierarchy(TOP); + boolean receiverHasManualAnno = + receiverAnno != null && !AnnotationUtils.areSameByName(BOTTOM, receiverAnno); + AnnotatedTypeMirror returnType = t.getReturnType(); + AnnotationMirror returnAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); + boolean returnHasManualAnno = + returnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, returnAnno); + + // inherit supertype annotations + + Map overriddenMethods = + AnnotatedTypes.overriddenMethods( + elements, CollectionOwnershipAnnotatedTypeFactory.this, t.getElement()); + + if (overriddenMethods != null) { + for (ExecutableElement superElt : overriddenMethods.values()) { + AnnotatedExecutableType annotatedSuperMethod = + CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(superElt); + + AnnotatedDeclaredType superReceiver = annotatedSuperMethod.getReceiverType(); + AnnotationMirror superReceiverAnno = superReceiver.getEffectiveAnnotationInHierarchy(TOP); + boolean superReceiverHasManualAnno = + superReceiverAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno); + if (!receiverHasManualAnno) { + if (superReceiverHasManualAnno) { + receiver.replaceAnnotation(superReceiverAnno); + } + } + + AnnotatedTypeMirror superReturnType = annotatedSuperMethod.getReturnType(); + AnnotationMirror superReturnAnno = superReturnType.getEffectiveAnnotationInHierarchy(TOP); + boolean superReturnHasManualAnno = + superReturnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno); + if (!returnHasManualAnno) { + if (superReturnHasManualAnno) { + returnType.replaceAnnotation(superReturnAnno); + } + } + + List superParams = + annotatedSuperMethod.getParameterTypes(); + if (params.size() == superParams.size()) { + for (int i = 0; i < superParams.size(); i++) { + AnnotationMirror superParamAnno = + superParams.get(i).getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror paramAnno = params.get(i).getEffectiveAnnotationInHierarchy(TOP); + boolean paramHasManualAnno = + paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); + boolean superParamHasManualAnno = + superParamAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno); + if (!paramHasManualAnno) { + if (superParamHasManualAnno) { + params.get(i).replaceAnnotation(superParamAnno); + } + } + } + } + } + } if (isResourceCollection(returnType.getUnderlyingType())) { AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); From 80cd91aad298f1c569003d114d9e7d2773ae50fc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 22 Jul 2025 19:37:50 +0200 Subject: [PATCH 238/374] formating --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 8294c67f29e3..f8a5ee4d64fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3554,7 +3554,7 @@ private Set analyzeTypeOfCollectionElement( boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock; store = cmAtf.getStoreAfterConditionalBlock(conditionalBlock, thenSuccessor); } else if (lastLoopBodyBlock.getLastNode() == null) { - throw new BugInCF("Loop Body Analysis -- Block " + lastLoopBodyBlock + " has no nodes"); + throw new BugInCF("Loop Body Analysis -- Block " + lastLoopBodyBlock + " has no nodes"); // store = cmAtf.getStoreAfterBlock(lastLoopBodyBlock); } else { store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); From 9a4d4ef1361bdf398dfb82e8dcb64490b062c4c7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 22 Jul 2025 23:28:15 +0200 Subject: [PATCH 239/374] suzanne's fix for the afu build file --- annotation-file-utilities/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/annotation-file-utilities/build.gradle b/annotation-file-utilities/build.gradle index 8325b3d1b93c..4f86d5ab73fc 100644 --- a/annotation-file-utilities/build.gradle +++ b/annotation-file-utilities/build.gradle @@ -26,7 +26,7 @@ java { dependencies { // Annotations in checker-qual.jar are used, but no checkers are (currently) run on the code. - compileOnly 'org.checkerframework:checker-qual:3.49.5' + compileOnly project(':checker-qual') implementation 'org.plumelib:options:2.0.3' implementation 'org.plumelib:plume-util:1.11.0' @@ -37,7 +37,7 @@ dependencies { } implementation 'org.ow2.asm:asm:9.8' testImplementation group: 'junit', name: 'junit', version: '4.13.2' - testImplementation 'org.checkerframework:checker-qual:3.49.5' + testImplementation project(':checker-qual') } From 33d7b647050336956036971be56a0d251873c7d1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 12:25:35 +0200 Subject: [PATCH 240/374] annotate afu for collection ownership checker --- .../afu/annotator/find/Insertions.java | 3 ++- .../afu/scenelib/io/ASTPath.java | 9 +++++---- .../scenelib/util/coll/LinkedHashKeyedSet.java | 17 ++++++++++++----- .../afu/scenelib/util/coll/WrapperMap.java | 5 +++-- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java index a975fdcd7997..4169e6fa790d 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java @@ -47,6 +47,7 @@ import org.checkerframework.afu.scenelib.type.DeclaredType; import org.checkerframework.afu.scenelib.type.Type; import org.checkerframework.checker.interning.qual.Interned; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.objectweb.asm.TypePath; /** @@ -210,7 +211,7 @@ public boolean hasNext() { } @Override - public Insertion next() { + public @NotOwning Insertion next() { if (hasNext()) { return iiter.next(); } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java index b2e1536a0c14..ee77d92d9717 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java @@ -61,6 +61,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import org.checkerframework.afu.annotator.find.CaseUtils; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.plumelib.util.ArraysPlume; /** A path through the AST. */ @@ -1460,18 +1461,18 @@ private static > S extend(T el, S s0) { } } - public boolean isEmpty() { + public boolean isEmpty(@NotOwningCollection ImmutableStack this) { return size == 0; } - public E peek() { + public E peek(@NotOwningCollection ImmutableStack this) { if (isEmpty()) { throw new IllegalStateException("peek() on empty stack"); } return elem; } - public ImmutableStack pop() { + public ImmutableStack pop(@NotOwningCollection ImmutableStack this) { if (isEmpty()) { throw new IllegalStateException("pop() on empty stack"); } @@ -1482,7 +1483,7 @@ public ImmutableStack push(E elem) { return extend(elem, this); } - public int size() { + public int size(@NotOwningCollection ImmutableStack this) { return size; } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index f20dd9d0859b..0db5d50f641a 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -5,6 +5,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import org.checkerframework.checker.collectionownership.qual.OwningCollection; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; /** * A simple implementation of {@link KeyedSet} backed by an insertion-order {@link @@ -44,7 +47,7 @@ public boolean hasNext() { } @Override - public V next() { + public @NotOwning V next() { return itr.next(); } @@ -71,7 +74,7 @@ public T[] toArray(T[] a) { return theValues.toArray(a); } - private boolean checkAdd(int behavior, V old) { + private boolean checkAdd(@OwningCollection LinkedHashKeyedSet this, int behavior, V old) { switch (behavior) { case REPLACE: remove(old); @@ -90,7 +93,11 @@ private static boolean eq(Object x, Object y) { } @Override - public V add(V o, int conflictBehavior, int equalBehavior) { + public V add( + @OwningCollection LinkedHashKeyedSet this, + V o, + int conflictBehavior, + int equalBehavior) { K key = keyer.getKeyFor(o); V old = theMap.get(key); if (old == null @@ -100,12 +107,12 @@ public V add(V o, int conflictBehavior, int equalBehavior) { } @Override - public boolean add(V o) { + public boolean add(@Owning V o) { return add(o, THROW_EXCEPTION, IGNORE) == null; } @Override - public boolean remove(Object o) { + public boolean remove(@OwningCollection LinkedHashKeyedSet this, Object o) { return theValues.remove(o); } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 3d88f84d172c..a41c8982024d 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.mustcall.qual.NotOwning; /** * A {@link WrapperMap} is a map all of whose methods delegate by default to those of a supplied @@ -39,7 +40,7 @@ public Set> entrySet() { } @Override - public V get(Object key) { + public @NotOwning V get(Object key) { return back.get(key); } @@ -54,7 +55,7 @@ public Set keySet() { } @Override - public V put(K key, V value) { + public @NotOwning V put(K key, V value) { return back.put(key, value); } From 3347442d470e35048241dce9ce8b0c215550eb89 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 12:25:49 +0200 Subject: [PATCH 241/374] warning suppressions in afu for collection ownership checker --- .../afu/annotator/find/Insertions.java | 1 + .../afu/annotator/find/TreeFinder.java | 10 ++++++++-- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 4 ++++ .../afu/scenelib/util/coll/VivifyingMap.java | 4 ++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java index 4169e6fa790d..4e1067bca863 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java @@ -185,6 +185,7 @@ public int size() { return size; } + @SuppressWarnings("collectionownership:assignment") @Override public Iterator iterator() { return new Iterator() { diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java index b63ac59ba277..3b641f2f1dc1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java @@ -990,7 +990,10 @@ private boolean wildcardLast(List location) { * @param p list of insertions not yet placed */ @Override - @SuppressWarnings("interning:not.interned") // reference equality check in assertion + @SuppressWarnings({ + "interning:not.interned", // reference equality check in assertion + "collectionownership:method.invocation" + }) // Iterator#remove() on a PolyOwningCollection iterator public Void scan(Tree node, List p) { if (node == null || p.isEmpty()) { return null; @@ -1831,7 +1834,10 @@ private void addNewType(NewInsertion neu, NewArrayTree newArray) { neu.getInnerTypeInsertions(), neu.getType(), neu.getCriteria().getASTPath()); } - @SuppressWarnings("interning:not.interned") + @SuppressWarnings({ + "interning:not.interned", + "collectionownership:method.invocation" + }) // Iterator#remove() on a PolyOwningCollection iterator private void addConstructor(TreePath path, ConstructorInsertion cons, MethodTree method) { ReceiverInsertion recv = cons.getReceiverInsertion(); assert method == (MethodTree) path.getLeaf(); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 0db5d50f641a..017ac03ba5b2 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -39,6 +39,10 @@ public boolean contains(Object o) { } private class KeyedSetIterator implements Iterator { + // the iterator is @PolyOwningCollection and thus the assignment + // reports an error. Since however it is an iterator over V, which + // has bottom as its upper bound, this is a false positive. + @SuppressWarnings("collectionownership:assignment") private final Iterator itr = theValues.iterator(); @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index d3e06819bc15..febc32f6c18b 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -46,6 +46,10 @@ public V getVivify(K k) { protected abstract V createValueFor(K k); /** Prunes this map by deleting entries with empty values. */ + // ei.remove() reports an error, since ei is @PolyOwningCollection. However, since + // the Map has bottom type var upper bound, the iterator is always going to be bottom + // in instances, so this is a false positive. + @SuppressWarnings("collectionownership:method.invocation") public void prune() { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { From 555194ed21db3ae1064e022df80a8bcdb9e9a539 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 16:07:00 +0200 Subject: [PATCH 242/374] add poly annotation as field --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index fa3c20ba861c..6f891d281112 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -88,6 +88,9 @@ public class CollectionOwnershipAnnotatedTypeFactory */ public final AnnotationMirror BOTTOM; + /** The {@code @}{@link PolyOwningCollection}{@code ()} polymorphic annotation. */ + public final AnnotationMirror POLY; + /** The value element of the {@code @}{@link CollectionFieldDestructor} annotation. */ private final ExecutableElement collectionFieldDestructorValueElement = TreeUtils.getMethod(CollectionFieldDestructor.class, "value", 0, processingEnv); @@ -174,6 +177,7 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { OWNINGCOLLECTIONWITHOUTOBLIGATION = AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); + POLY = AnnotationBuilder.fromClass(elements, PolyOwningCollection.class); this.postInit(); } From 0f25e74bba7f2b0b40bb08356bf77b5741b72d1f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 16:07:20 +0200 Subject: [PATCH 243/374] don't inherit polyoc annotation --- ...CollectionOwnershipAnnotatedTypeFactory.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 6f891d281112..bc457acf1b94 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -609,10 +609,11 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(superElt); AnnotatedDeclaredType superReceiver = annotatedSuperMethod.getReceiverType(); - AnnotationMirror superReceiverAnno = superReceiver.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror superReceiverAnno = superReceiver.getPrimaryAnnotationInHierarchy(TOP); boolean superReceiverHasManualAnno = superReceiverAnno != null - && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno); + && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno) + && !AnnotationUtils.areSameByName(POLY, superReceiverAnno); if (!receiverHasManualAnno) { if (superReceiverHasManualAnno) { receiver.replaceAnnotation(superReceiverAnno); @@ -620,9 +621,11 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { } AnnotatedTypeMirror superReturnType = annotatedSuperMethod.getReturnType(); - AnnotationMirror superReturnAnno = superReturnType.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror superReturnAnno = superReturnType.getPrimaryAnnotationInHierarchy(TOP); boolean superReturnHasManualAnno = - superReturnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno); + superReturnAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno) + && !AnnotationUtils.areSameByName(POLY, superReturnAnno); if (!returnHasManualAnno) { if (superReturnHasManualAnno) { returnType.replaceAnnotation(superReturnAnno); @@ -634,12 +637,14 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { if (params.size() == superParams.size()) { for (int i = 0; i < superParams.size(); i++) { AnnotationMirror superParamAnno = - superParams.get(i).getEffectiveAnnotationInHierarchy(TOP); + superParams.get(i).getPrimaryAnnotationInHierarchy(TOP); AnnotationMirror paramAnno = params.get(i).getEffectiveAnnotationInHierarchy(TOP); boolean paramHasManualAnno = paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); boolean superParamHasManualAnno = - superParamAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno); + superParamAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno) + && !AnnotationUtils.areSameByName(POLY, superParamAnno); if (!paramHasManualAnno) { if (superParamHasManualAnno) { params.get(i).replaceAnnotation(superParamAnno); From a4f1a3f38fb362fbf34d93d61936509b11c22255 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 16:16:37 +0200 Subject: [PATCH 244/374] propagate annotations from parameter decl to use site --- .../CollectionOwnershipAnnotatedTypeFactory.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index bc457acf1b94..bc1059fe8642 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -730,9 +730,18 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { type.replaceAnnotation(OWNINGCOLLECTION); } } else if (isParam) { - AnnotationMirror paramAnno = type.getEffectiveAnnotationInHierarchy(TOP); - if (paramAnno == null || AnnotationUtils.areSameByName(BOTTOM, paramAnno)) { - type.replaceAnnotation(NOTOWNINGCOLLECTION); + // propagate annotation computed for parameter declaration + // to the use site + Element enclosingElement = elt.getEnclosingElement(); + ExecutableElement method = (ExecutableElement) enclosingElement; + AnnotatedExecutableType annotatedMethod = + CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(method); + List params = method.getParameters(); + List paramTypes = annotatedMethod.getParameterTypes(); + for (int i = 0; i < params.size(); i++) { + if (params.get(i).getSimpleName() == elt.getSimpleName()) { + type.replaceAnnotation(paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP)); + } } } } From 64b18550431e06357ebdb9121c35b307a4c476e2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 23 Jul 2025 17:18:11 +0200 Subject: [PATCH 245/374] adapt annos&suppressions for afu subproject after not inheriting polyOC anymore --- .../afu/annotator/find/Insertions.java | 4 ++-- .../afu/annotator/find/TreeFinder.java | 10 ++-------- .../org/checkerframework/afu/scenelib/io/ASTPath.java | 3 ++- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 7 ++----- .../afu/scenelib/util/coll/VivifyingMap.java | 4 ---- 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java index 4e1067bca863..4134d7c424c9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/Insertions.java @@ -46,6 +46,7 @@ import org.checkerframework.afu.scenelib.type.BoundedType; import org.checkerframework.afu.scenelib.type.DeclaredType; import org.checkerframework.afu.scenelib.type.Type; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.objectweb.asm.TypePath; @@ -185,9 +186,8 @@ public int size() { return size; } - @SuppressWarnings("collectionownership:assignment") @Override - public Iterator iterator() { + public @PolyOwningCollection Iterator iterator(@PolyOwningCollection Insertions this) { return new Iterator() { private Iterator>> miter = store.values().iterator(); // These two fields are initially empty iterators, but are set the first time that hasNext is diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java index 3b641f2f1dc1..b63ac59ba277 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/annotator/find/TreeFinder.java @@ -990,10 +990,7 @@ private boolean wildcardLast(List location) { * @param p list of insertions not yet placed */ @Override - @SuppressWarnings({ - "interning:not.interned", // reference equality check in assertion - "collectionownership:method.invocation" - }) // Iterator#remove() on a PolyOwningCollection iterator + @SuppressWarnings("interning:not.interned") // reference equality check in assertion public Void scan(Tree node, List p) { if (node == null || p.isEmpty()) { return null; @@ -1834,10 +1831,7 @@ private void addNewType(NewInsertion neu, NewArrayTree newArray) { neu.getInnerTypeInsertions(), neu.getType(), neu.getCriteria().getASTPath()); } - @SuppressWarnings({ - "interning:not.interned", - "collectionownership:method.invocation" - }) // Iterator#remove() on a PolyOwningCollection iterator + @SuppressWarnings("interning:not.interned") private void addConstructor(TreePath path, ConstructorInsertion cons, MethodTree method) { ReceiverInsertion recv = cons.getReceiverInsertion(); assert method == (MethodTree) path.getLeaf(); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java index ee77d92d9717..512b60db4e2c 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java @@ -62,6 +62,7 @@ import java.util.Objects; import org.checkerframework.afu.annotator.find.CaseUtils; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.plumelib.util.ArraysPlume; /** A path through the AST. */ @@ -296,7 +297,7 @@ public static Comparator getComparator() { // TODO: replace w/ skip list? @Override - public Iterator iterator() { + public @PolyOwningCollection Iterator iterator(@PolyOwningCollection ASTPath this) { ImmutableStack s = this; int n = size(); ASTEntry[] a = new ASTEntry[n]; diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 017ac03ba5b2..9217cfba63b4 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -6,6 +6,7 @@ import java.util.LinkedHashMap; import java.util.Map; import org.checkerframework.checker.collectionownership.qual.OwningCollection; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; @@ -39,10 +40,6 @@ public boolean contains(Object o) { } private class KeyedSetIterator implements Iterator { - // the iterator is @PolyOwningCollection and thus the assignment - // reports an error. Since however it is an iterator over V, which - // has bottom as its upper bound, this is a false positive. - @SuppressWarnings("collectionownership:assignment") private final Iterator itr = theValues.iterator(); @Override @@ -64,7 +61,7 @@ public void remove() { } @Override - public Iterator iterator() { + public Iterator iterator(@PolyOwningCollection LinkedHashKeyedSet this) { return new KeyedSetIterator(); } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java index febc32f6c18b..d3e06819bc15 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/VivifyingMap.java @@ -46,10 +46,6 @@ public V getVivify(K k) { protected abstract V createValueFor(K k); /** Prunes this map by deleting entries with empty values. */ - // ei.remove() reports an error, since ei is @PolyOwningCollection. However, since - // the Map has bottom type var upper bound, the iterator is always going to be bottom - // in instances, so this is a false positive. - @SuppressWarnings("collectionownership:method.invocation") public void prune() { // It would be cleaner to write // for (Map.Entry entry : entrySet()) { From 4433b46e4def15002bbce482925b9d32bd313455 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 23 Jul 2025 09:15:39 -0700 Subject: [PATCH 246/374] Add Javadoc --- .../afu/scenelib/io/ASTPath.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java index 82d2549eb355..99432e546732 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java @@ -1462,6 +1462,11 @@ public boolean isEmpty() { return size == 0; } + /** + * Returns the top element of the stack, without modifying the stack. + * + * @return the top element of the stack + */ public E peek() { if (isEmpty()) { throw new IllegalStateException("peek() on empty stack"); @@ -1469,6 +1474,11 @@ public E peek() { return elem; } + /** + * Returns all of the stack except the top element. + * + * @return all of the stack except the top element + */ public ImmutableStack pop() { if (isEmpty()) { throw new IllegalStateException("pop() on empty stack"); @@ -1484,7 +1494,12 @@ public int size() { return size; } - /** Return the index-th element of this stack. */ + /** + * Returns the index-th element of this stack. + * + * @param index which element to return + * @return the index-th element of this stack + */ public E get(int index) { int n = size(); From b39a77ab7507130ff636c02cbf12db3c3f331030 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 23 Jul 2025 09:48:10 -0700 Subject: [PATCH 247/374] Add Javadoc --- .../org/checkerframework/afu/scenelib/io/ASTPath.java | 10 ++++++++++ .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java index 243e055d7053..f989c7bddf47 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java @@ -1462,6 +1462,11 @@ private static > S extend(T el, S s0) { } } + /** + * Returns true if the stack is empty. + * + * @return true if the stack is empty + */ public boolean isEmpty(@NotOwningCollection ImmutableStack this) { return size == 0; } @@ -1494,6 +1499,11 @@ public ImmutableStack push(E elem) { return extend(elem, this); } + /** + * Returns the size: the number of elements in the stack. + * + * @return the size of this stack + */ public int size(@NotOwningCollection ImmutableStack this) { return size; } diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 9217cfba63b4..61e270b17397 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -75,6 +75,14 @@ public T[] toArray(T[] a) { return theValues.toArray(a); } + /** + * Prepares for adding an element to this. Removes the given element if {@code behavior} is + * REPLACE. + * + * @param behavior what action this method should take + * @param old the element to be removed, if {@code behavior} is REPLACE + * @return true if an element was removed + */ private boolean checkAdd(@OwningCollection LinkedHashKeyedSet this, int behavior, V old) { switch (behavior) { case REPLACE: From fe92dab95dd575d4f7c716d2de752094f48de896 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 24 Jul 2025 09:19:22 -0700 Subject: [PATCH 248/374] Use %n, not \n, in format strings --- .../checker/collectionownership/messages.properties | 2 +- .../checkerframework/checker/resourceleak/messages.properties | 2 +- checker/tests/resourceleak/Issue4815.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 4ea7e9890126..48dc1f4750f7 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,4 +1,4 @@ -unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s +unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. illegal.type.annotation=The annotation %s is for internal use only and not an allowed user annotation. static.resource.collection.field=The resource collection field %s is static, which is not supported by the checker. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index d95fe86e7ed6..8ac9bae77cf7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -8,7 +8,7 @@ destructor.exceptional.postcondition=Method %s must resolve the must-call obliga mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope without being assigned into an owning field of this object (if this is a constructor) or returned.%nReason for going out of scope: %s mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in pairs (one on a return type and one on a parameter type).%nBut %s required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s -unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.\nReason for going out of scope: %s +unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field not owned by this class instance. illegal.owningcollection.field.assignment=The right-hand side of a field initializer assignment to an owning resource collection field must be of type @OwningCollectionWithoutObligation, but is of type %s. Consider moving the assignment into the constructor. diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java index 602771335b97..d88cda53e23d 100644 --- a/checker/tests/resourceleak/Issue4815.java +++ b/checker/tests/resourceleak/Issue4815.java @@ -8,7 +8,7 @@ public class Issue4815 { public void initialize( List list, @Owning @MustCall("initialize") T object) { object.initialize(); - // list resolves to List<@MustCall Component> and thus cannot accept + // `list` resolves to List<@MustCall Component> and thus cannot accept // an element of non-empty @MustCall type enforced by the Java // typechecker. This is an unfortunate consequence of the otherwise // elegant extension of the RLC to collections, which doesn't detect From 10a30c2d8005767948f99dc4359aa707afda04bd Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 24 Jul 2025 12:12:43 -0700 Subject: [PATCH 249/374] Documentation improvements --- .../qual/CollectionFieldDestructor.java | 10 ++++++---- .../qual/CreatesCollectionObligation.java | 12 +++++++---- .../qual/NotOwningCollection.java | 10 ++++------ .../qual/OwningCollection.java | 14 ++++++------- .../qual/OwningCollectionBottom.java | 8 +++++--- .../OwningCollectionWithoutObligation.java | 20 +++++++------------ .../qual/PolyOwningCollection.java | 1 + .../collectionownership/messages.properties | 5 ++--- docs/manual/resource-leak-checker.tex | 3 +++ 9 files changed, 42 insertions(+), 41 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java index f7966b0908d6..0453b4ff3b30 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CollectionFieldDestructor.java @@ -9,8 +9,8 @@ import org.checkerframework.framework.qual.PostconditionAnnotation; /** - * Indicates that the given resource collection fields are destructed within this method, i.e. this - * method calls all required methods on their elements. + * The annotated method destructs the given resource collection fields. That is, this method calls + * all required methods on their elements. * *


  *  {@literal @}CollectionFieldDestructor("socketList")
@@ -22,6 +22,8 @@
  *    }
  *  }
  * 
+ * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -30,9 +32,9 @@ @InheritedAnnotation public @interface CollectionFieldDestructor { /** - * Returns the resource collection field whose collection obligation the destructor fulfills. + * Returns the resource collection fields whose collection obligation the destructor fulfills. * - * @return the resource collection field whose collection obligation the destructor fulfills + * @return the resource collection fields whose collection obligation the destructor fulfills */ String[] value(); } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java index 04d9e7f040b5..203db61aa4d8 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -8,14 +8,18 @@ /** * A method carrying this annotation creates a {@code CollectionObligation} for the receiver - * collection. If the receiver of a method annotated with this annotation is of type - * {@code @OwningCollectionWithoutObligation}, it is unrefined to {@code @OwningCollection}, and a - * CollectionObligation is created for each {@code @MustCall} method of the type variable of the - * receiver. + * collection. + * + *

Consider a call to a {@code CreatesCollectionObligation}-annotated method. If the receiver is + * of type {@code @OwningCollectionWithoutObligation}, it is unrefined to {@code @OwningCollection}, + * and a CollectionObligation is created for each {@code @MustCall} method of the type variable of + * the receiver. * *

This annotation should only be used on method and constructor declarations of collections, as * defined by the CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code * java.util.Iterator} implementations. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java index 8aca48f9912d..297bbe4b28cd 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -7,13 +7,11 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * The top qualifier in the Collection Ownership type hierarchy. + * An expression of type {@code NotOwningCollection} is a non-owning reference to a resource + * collection/array. Because it does not own the underlying collection/array, it cannot add or + * remove elements from it. * - *

An expression of type {@code NotOwningCollection} is a resource collection/array, which may - * not own the underlying collection/array, and thus cannot add or remove elements from it. - * - *

This annotation can be enforced by running the Resource Leak Checker. It enforces that the - * expression is not used to add to or remove elements from the underlying collection/array. + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java index cb02b5f786e1..00cdfda0983d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -7,16 +7,14 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * An expression of type {@code @OwningCollection} is a resource collection/array, which definitely + * An expression of type {@code @OwningCollection} is a resource collection/array that definitely * owns the underlying collection/array. It can add or remove elements from the collection/array. * - *

This annotation can be enforced by running the Resource Leak Checker. The dataflow analysis - * running after the type checker tracks at least one owner per underlying resource - * collection/array. In particular, if an expression has type {@code @OwningCollection}, it is also - * tracked as an owner by the dataflow analysis, which checks that it (or one of its aliases) calls - * the methods in the {@code @MustCall} type of its elements on all of its elements before leaving - * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an - * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). + *

The annotated expression (or one of its aliases) must either call the methods in the + * {@code @MustCall} type of its elements on all of its elements, or pass on the obligation to + * another expression. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index 5cd6121bf855..40c2cbcbf345 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -10,10 +10,12 @@ import org.checkerframework.framework.qual.TypeUseLocation; /** - * The bottom qualifier of the Collection Ownership type hierarchy, and the default qualifier. + * The bottom qualifier of the Collection Ownership type hierarchy. It is the default qualifier. * - *

An expression of type {@code @OwningCollectionBottom} is either not a collection/array, or a - * collection/array, whose elements have empty {@code @MustCall} type. + *

An expression of type {@code @OwningCollectionBottom} is either not a collection/array or is a + * collection/array whose elements have empty {@code @MustCall} type. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java index 8d57f9130de6..f9842c805d73 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java @@ -7,22 +7,16 @@ import org.checkerframework.framework.qual.SubtypeOf; /** - * An expression of type {@code @OwningCollectionWithoutObligation} is a resource collection/arary, - * which definitely owns the underlying collection/array and has definitely called all of the - * methods in the {@code @MustCall} type of its elements on all of its elements. + * An expression of type {@code @OwningCollectionWithoutObligation} is a resource collection/array, + * which definitely owns the underlying collection/array. Furthermore, every element has already + * called all of the methods in its {@code @MustCall} type. * - *

This annotation exists such that for a destructor method {@code d} of a class {@code C} with - * an {@code @OwningCollection} field {@code f}, the post-condition of said destructor method can be - * of the form {@code @EnsuresQualifier(expression = "this.f", qualifier = + *

Consider a destructor method {@code d} of a class {@code C} with an {@code @OwningCollection} + * field {@code f}. The post-condition of the destructor method is + * {@code @EnsuresQualifier(expression = "this.f", qualifier = * OwningCollectionWithoutObligation.class)}. * - *

This annotation can be enforced by running the Resource Leak Checker. The dataflow analysis - * running after the type checker tracks at least one owner per underlying resource - * collection/array. In particular, if an expression has type {@code @OwningCollection}, it is also - * tracked as an owner by the dataflow analysis, which checks that it (or one of its aliases) calls - * the methods in the {@code @MustCall} type of its elements on all of its elements before leaving - * scope, or it passes on the obligation (by writing to an {@code @OwningCollection}, passing to an - * {@code @OwningCollection} parameter, or returning as an {@code @OwningCollection} return type). + * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java index 402419340f5c..696568b4dee9 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java @@ -10,6 +10,7 @@ /** * A polymorphic qualifier for the Collection-Ownership type system. * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ @Documented diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 48dc1f4750f7..0b32f731986d 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,5 +1,4 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. -illegal.type.annotation=The annotation %s is for internal use only and not an allowed user annotation. -static.resource.collection.field=The resource collection field %s is static, which is not supported by the checker. -The field is unsoundly treated as non-static by the checker. +illegal.type.annotation=Users may not write %s. +static.resource.collection.field=The static field %s is unsoundly treated as non-static. diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index a8701a67a2f8..9a8a9449d62e 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -141,6 +141,8 @@ \end{description} +\subsectionAndLabel{Resource Leak Checker annotations for collections}{resource-leak-annotations-collections} + The Resource Leak Checker also supports collections of resources. The following annotations express ownership over resource collections. \begin{description} @@ -160,6 +162,7 @@ \end{description} + \sectionAndLabel{Example of how safe resource usage is verified}{resource-leak-example} Consider the following example of safe use of a \, in which the comments indicate the From ceebfff8137d08a4e00bd3b00bfca368a232184f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 04:52:19 -0700 Subject: [PATCH 250/374] Documentation improvements --- ...llectionOwnershipAnnotatedTypeFactory.java | 47 ++++++++++--------- .../CollectionOwnershipChecker.java | 2 +- .../MustCallAnnotatedTypeFactory.java | 18 +++---- .../expression/IteratedCollectionElement.java | 7 +-- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index bc1059fe8642..111deddeb493 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -144,7 +144,7 @@ public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { /** * Returns the collection-obligation-fulfilling loop for which the given tree is the condition. * - * @param tree the tree that is potentially the condition for a fulfilling loop + * @param tree a tree that is potentially the condition for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given tree is the condition */ public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) { @@ -152,12 +152,12 @@ public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) } /** - * Returns the collection-obligation-fulfilling loop for which the given block is the conditional - * block for. + * Returns the collection-obligation-fulfilling loop for which the given block is the CFG + * conditional block. * * @param block the block that is potentially the conditional block for a fulfilling loop - * @return the collection-obligation-fulfilling loop for which the given block is the conditional - * block for + * @return the collection-obligation-fulfilling loop for which the given block is the CFG + * conditional block */ public static PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Block block) { return conditionalBlockToFulfillingLoopMap.get(block); @@ -198,34 +198,36 @@ protected Set> createSupportedTypeQualifiers() { } /** - * Fetches the store from the results of dataflow for {@code first}. If {@code afterFirstStore} is - * true, then the store after {@code first} is returned; if {@code afterFirstStore} is false, the - * store before {@code succ} is returned. + * Fetches the store from the results of dataflow for {@code firstBlock}. If {@code + * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code + * afterFirstStore} is false, the store before {@code succBlock} is returned. * * @param afterFirstStore whether to use the store after the first block or the store before its - * successor, succ - * @param first a block - * @param succ first's successor + * successor, succBlock + * @param firstBlock a block + * @param succBlock {@code firstBlock}'s successor * @return the appropriate CollectionOwnershipStore, populated with MustCall annotations, from the * results of running dataflow */ public CollectionOwnershipStore getStoreForBlock( - boolean afterFirstStore, Block first, Block succ) { - return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); + boolean afterFirstStore, Block firstBlock, Block succBlock) { + return afterFirstStore + ? flowResult.getStoreAfter(firstBlock) + : flowResult.getStoreBefore(succBlock); } @Override public void postAnalyze(ControlFlowGraph cfg) { ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); - RLCCalledMethodsAnnotatedTypeFactory cmAtf = - (RLCCalledMethodsAnnotatedTypeFactory) - ResourceLeakUtils.getRLCCalledMethodsChecker(this).getTypeFactory(); rlc.setRoot(root); MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc, false); mustCallConsistencyAnalyzer.analyze(cfg); + RLCCalledMethodsAnnotatedTypeFactory cmAtf = + (RLCCalledMethodsAnnotatedTypeFactory) + ResourceLeakUtils.getRLCCalledMethodsChecker(this).getTypeFactory(); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for - // finalizer methods and @InheritableMustCall annotations for the class declarations. + // finalizer methods, and @InheritableMustCall annotations for the class declarations. if (cmAtf.getWholeProgramInference() != null) { if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); @@ -236,12 +238,13 @@ public void postAnalyze(ControlFlowGraph cfg) { } /** - * Returns whether the given type is a resource collection. This overload should be used before - * computation of AnnotatedTypeMirrors is completed, in particular in - * addComputedTypeAnnotations(AnnotatedTypeMirror). + * Returns true if the given type is a resource collection: a type assignable from {@code + * Collection} whose single type var has non-empty MustCall type. + * + *

This overload should be used before computation of AnnotatedTypeMirrors is completed, in + * particular in addComputedTypeAnnotations(AnnotatedTypeMirror). * - *

That is, whether the given type is a type assignable from java.util.Collection, whose only - * type var has non-empty MustCall type. + *

That is, whether the given type is * * @param t the AnnotatedTypeMirror * @return whether t is a resource collection diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index dde18fc23400..28dbda33f176 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -8,7 +8,7 @@ /** * This typechecker tracks at most one owning variable per resource collection using an ownership - * type system. The resource leak checker verifies that (at least) the determined owner fulfills the + * type system. The Resource Leak Checker verifies that (at least) the determined owner fulfills the * calling obligations of its elements. */ public class CollectionOwnershipChecker extends BaseTypeChecker { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 57d626a43827..5babed29a449 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -585,19 +585,21 @@ protected QualifierHierarchy createQualifierHierarchy() { } /** - * Fetches the store from the results of dataflow for {@code first}. If {@code afterFirstStore} is - * true, then the store after {@code first} is returned; if {@code afterFirstStore} is false, the - * store before {@code succ} is returned. + * Fetches the store from the results of dataflow for {@code firstBlock}. If {@code + * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code + * afterFirstStore} is false, the store before {@code succBlock} is returned. * * @param afterFirstStore whether to use the store after the first block or the store before its - * successor, succ - * @param first a block - * @param succ first's successor + * successor, {@code succBlock} + * @param firstBlock a CFG block + * @param succBlock {@code firstBlock}'s successor * @return the appropriate CFStore, populated with MustCall annotations, from the results of * running dataflow */ - public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ) { - return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); + public CFStore getStoreForBlock(boolean afterFirstStore, Block firstBlock, Block succBlock) { + return afterFirstStore + ? flowResult.getStoreAfter(firstBlock) + : flowResult.getStoreBefore(succBlock); } /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index 5a4e8824f357..7e0f5bc18575 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -10,7 +10,7 @@ * collection-obligation-fulfilling loop, for example {@code o} in {@code for (Object o: list) { }} */ public class IteratedCollectionElement extends JavaExpression { - /** The cfg node for this collection element. */ + /** The CFG node for this collection element. */ public final Node node; /** The AST node for this collection element. */ @@ -35,10 +35,7 @@ public boolean equals(@Nullable Object obj) { return false; } IteratedCollectionElement other = (IteratedCollectionElement) obj; - return other.tree.equals(this.tree) - || other.tree == this.tree - || other.node == this.node - || other.node.equals(this.node); + return other.tree.equals(this.tree) || other.node.equals(this.node); } // /** From 9549bfc8aa070c9ff8fe0e7e0e57712ba5f06c40 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 05:02:37 -0700 Subject: [PATCH 251/374] Use "true if" instead of "whether" --- ...llectionOwnershipAnnotatedTypeFactory.java | 22 +++++++++---------- .../CollectionOwnershipStore.java | 2 +- .../checker/mustcall/MustCallVisitor.java | 13 +++++------ .../resourceleak/ResourceLeakUtils.java | 12 +++++----- .../MethodValAnnotatedTypeFactory.java | 2 +- .../framework/type/SubtypeVisitHistory.java | 2 +- .../checkerframework/javacutil/TreeUtils.java | 4 ++-- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 111deddeb493..f584a4a87ec9 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -202,7 +202,7 @@ protected Set> createSupportedTypeQualifiers() { * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code * afterFirstStore} is false, the store before {@code succBlock} is returned. * - * @param afterFirstStore whether to use the store after the first block or the store before its + * @param afterFirstStore if true, use the store after the first block or the store before its * successor, succBlock * @param firstBlock a block * @param succBlock {@code firstBlock}'s successor @@ -247,7 +247,7 @@ public void postAnalyze(ControlFlowGraph cfg) { *

That is, whether the given type is * * @param t the AnnotatedTypeMirror - * @return whether t is a resource collection + * @return true if t is a resource collection */ public boolean isResourceCollection(TypeMirror t) { if (t == null) return false; @@ -256,9 +256,9 @@ public boolean isResourceCollection(TypeMirror t) { } /** - * Whether the given element is a resource collection field that is {@code @OwningCollection} by - * declaration, which is the default behavior, i.e. with no different collection ownership - * annotation. + * Returns true if the given element is a resource collection field that is + * {@code @OwningCollection} by declaration, which is the default behavior, i.e. with no different + * collection ownership annotation. * * @param elt the element * @return true if the element is a resource collection field that is {@code @OwningCollection} by @@ -287,7 +287,7 @@ public boolean isOwningCollectionField(Element elt) { } /** - * Whether the given element is a resource collection field. + * Returns true if the given element is a resource collection field. * * @param elt the element * @return true if the element is a resource collection field. @@ -303,9 +303,9 @@ public boolean isResourceCollectionField(Element elt) { } /** - * Whether the given element is a resource collection parameter that is {@code @OwningCollection} - * by declaration, which is the default behavior, i.e. with no different collection ownership - * annotation. + * Returns true if the given element is a resource collection parameter that is + * {@code @OwningCollection} by declaration, which is the default behavior, i.e. with no different + * collection ownership annotation. * * @param elt the element * @return true if the element is a resource collection parameter that is @@ -333,13 +333,13 @@ public boolean isOwningCollectionParameter(Element elt) { } /** - * Returns whether the given AST tree is a resource collection. + * Returns true if the given AST tree is a resource collection. * *

That is, whether the given tree is of a type assignable from java.util.Collection, whose * only type var has non-empty MustCall type. * * @param tree the tree - * @return whether the tree is a resource collection + * @return true if the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { if (tree == null) return false; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 28afc44dc38f..09618e352cda 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -19,7 +19,7 @@ public class CollectionOwnershipStore extends CFAbstractStoreThat is, the names are equal. @@ -673,8 +672,8 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { * * @param idInHeader identifier from loop header * @param collectionName name of collection - * @return whether the given collection name is consistent with the identifier from the loop - * header. + * @return true if the given collection name is consistent with the identifier from the loop + * header */ private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collectionName) { if (idInHeader == null || collectionName == null) return false; @@ -683,12 +682,12 @@ private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collect } /** - * Returns whether the given tree is of the form collection.get(i), where i is the given index + * Returns true if the given tree is of the form collection.get(i), where i is the given index * name. * * @param tree the tree to check * @param index the index variable name - * @return whether the given tree is of the form collection.get(index) + * @return true if the given tree is of the form collection.get(index) */ private boolean isIthCollectionElement(Tree tree, Name index) { if (tree == null || index == null) return false; diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index b14e56ea431c..2239c4568d6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -238,8 +238,8 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno } else { throw new IllegalArgumentException( "Argument referenceChecker to" - + " ResourceLeakUtils#getCollectionOwnershipAnnotatedTypeFactory(referenceChecker) expected to" - + " be an RLC checker but is " + + " ResourceLeakUtils#getCollectionOwnershipAnnotatedTypeFactory(referenceChecker)" + + " expected to be an RLC checker but is " + className); } } @@ -262,7 +262,7 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno } /** - * Returns whether the given Element is a java.lang.Iterable or java.util.Iterator type by + * Returns true if the given Element is a java.lang.Iterable or java.util.Iterator type by * checking whether the raw type of the element is assignable from either. Returns false if * element is null, or has no valid type. * @@ -278,7 +278,7 @@ public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { } /** - * Returns whether the given {@link TypeMirror} is a java.lang.Iterable or java.util.Iterator + * Returns true if the given {@link TypeMirror} is a java.lang.Iterable or java.util.Iterator * subclass. This is determined by getting the class of the TypeMirror and checking whether it is * assignable from either. * @@ -295,12 +295,12 @@ public static boolean isCollection(TypeMirror type) { } /** - * Returns whether the given {@link TypeMirror} is a java.util.Iterator subclass. This is + * Returns true if the given {@link TypeMirror} is a java.util.Iterator subclass. This is * determined by getting the class of the TypeMirror and checking whether it is assignable from * java.util.Iterator. * * @param type the TypeMirror - * @return whether type is a java.util.Iterator + * @return true if type is a java.util.Iterator */ public static boolean isIterator(TypeMirror type) { if (type == null) return false; diff --git a/framework/src/main/java/org/checkerframework/common/reflection/MethodValAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/reflection/MethodValAnnotatedTypeFactory.java index a4a285b19cb0..e1a596b18b0c 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/MethodValAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/common/reflection/MethodValAnnotatedTypeFactory.java @@ -152,7 +152,7 @@ private AnnotationMirror createMethodVal(Collection sigs) { * Returns a list of class names for the given tree using the Class Val Checker. * * @param tree an ExpressionTree whose class names are requested - * @param mustBeExact whether @ClassBound may be read to produce the result; if false, + * @param mustBeExact true if @ClassBound may be read to produce the result; if false, * only @ClassVal may be read * @return list of class names or the empty list if no class names were found */ diff --git a/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java b/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java index f946e1f03df8..08f9af170d67 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java @@ -43,7 +43,7 @@ public SubtypeVisitHistory() { * @param type2 the second type * @param currentTop the top of the relevant type hierarchy; only annotations from that hierarchy * are considered - * @param isSubtype whether {@code type1} is a subtype of {@code type2}; if false, this method + * @param isSubtype true if {@code type1} is a subtype of {@code type2}; if false, this method * does nothing */ public void put( diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 83571022991a..cc3d3313f169 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1740,10 +1740,10 @@ && isNamedMethodCall("get", (MethodInvocationTree) tree)) { } /** - * Returns whether the given tree is of the form object.size() + * Returns true if the given tree is of the form object.size(). * * @param tree the tree to check - * @return whether the given tree is of the form object.size() + * @return true if the given tree is of the form object.size() */ public static boolean isSizeAccess(Tree tree) { return (tree instanceof MethodInvocationTree) From 116f596939c3edc049354811aecb5766f1ac7f1a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 05:51:11 -0700 Subject: [PATCH 252/374] Improve Javadoc style --- .../qual/EnsuresCalledMethodsOnException.java | 4 ++-- .../checker/lock/qual/EnsuresLockHeldIf.java | 2 +- .../CalledMethodsAnnotatedTypeFactory.java | 4 ++-- .../calledmethods/CalledMethodsTransfer.java | 4 ++-- .../EnsuresCalledMethodOnExceptionContract.java | 4 ++-- .../CollectionOwnershipAnnotatedTypeFactory.java | 8 ++++---- .../checker/fenum/FenumVisitor.java | 2 +- .../i18nformatter/I18nFormatterTreeUtil.java | 2 +- .../checker/index/upperbound/OffsetEquation.java | 2 +- .../checker/lock/LockVisitor.java | 6 +++--- .../CreatesMustCallForToJavaExpression.java | 2 +- .../checker/mustcall/MustCallVisitor.java | 8 ++++---- .../checker/nullness/KeyForPropagator.java | 2 +- .../checker/regex/RegexTransfer.java | 2 +- .../MustCallConsistencyAnalyzer.java | 16 ++++++++-------- .../RLCCalledMethodsAnnotatedTypeFactory.java | 2 +- .../RLCCalledMethodsChecker.java | 7 +++++-- .../RLCCalledMethodsVisitor.java | 6 +++--- .../checker/signedness/SignednessShifts.java | 2 +- .../checker/units/UnitsRelationsDefault.java | 6 +++--- .../checker/test/junit/NonEmptyTest.java | 2 +- .../dataflow/analysis/UnusedAbstractValue.java | 2 +- .../dataflow/busyexpr/BusyExprStore.java | 2 +- .../dataflow/busyexpr/BusyExprTransfer.java | 2 +- .../cfg/builder/CFGTranslationPhaseOne.java | 4 ++-- .../dataflow/cfg/node/AssignmentNode.java | 2 +- .../common/util/count/report/ReportChecker.java | 6 +++--- 27 files changed, 57 insertions(+), 54 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java index b9850da3c3e9..69eb403434f5 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java @@ -22,8 +22,8 @@ * public void callM(T t) { ... } * * - *

The callM method promises to always call {@code t.m()} before throwing any kind - * of {@link Exception}. + *

The {@code callM} method promises to always call {@code t.m()} before throwing any kind of + * {@link Exception}. * *

Note that {@code EnsuresCalledMethodsOnException} only describes behavior for {@link * Exception} (and by extension {@link RuntimeException}, {@link NullPointerException}, etc.) but diff --git a/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java index 6980ec887978..8317951c7989 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java @@ -35,7 +35,7 @@ * Returns Java expressions whose values are locks that are held after the method returns the * given result. * - * @return Java expressions whose values are locks that are held after the method returns the + * @return the Java expressions whose values are locks that are held after the method returns the * given result * @see Syntax of * Java expressions diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java index 07a5a10b333e..dbfdfd5ce3e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java @@ -499,7 +499,7 @@ public Set getExceptionalPostconditions( /** * Helper for {@link #getExceptionalPostconditions(ExecutableElement)} that parses a {@link - * EnsuresCalledMethodsOnException.List} annotation and stores the results in out. + * EnsuresCalledMethodsOnException.List} annotation and stores the results in {@code out}. * * @param annotation the annotation * @param out the output collection @@ -524,7 +524,7 @@ private void parseEnsuresCalledMethodOnExceptionListAnnotation( /** * Helper for {@link #getExceptionalPostconditions(ExecutableElement)} that parses a {@link - * EnsuresCalledMethodsOnException} annotation and stores the results in out. + * EnsuresCalledMethodsOnException} annotation and stores the results in {@code out}. * * @param annotation the annotation * @param out the output collection diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java index 85aec110e04c..07b1fa3a73d9 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java @@ -252,9 +252,9 @@ private void handleEnsuresCalledMethodsVarargs( } /** - * Update the given exceptionalStores for the {@link + * Update the given {@code exceptionalStores} for the {@link * org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException} annotations - * written on the given method. + * written on the given {@code method}. * * @param node a method invocation * @param method the method being invoked diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java index bdc6fd106015..0bddc54f3a62 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java @@ -20,8 +20,8 @@ public class EnsuresCalledMethodOnExceptionContract { private final String method; /** - * Create a new EnsuredCalledMethodOnException. Usually this should be constructed - * from a {@link org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException} + * Create a new {@code EnsuredCalledMethodOnException}. Usually this should be constructed from a + * {@link org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException} * appearing in the source code. * * @param expression the expression described by this postcondition diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f584a4a87ec9..86d151745196 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -132,7 +132,7 @@ public enum CollectionOwnershipType { new HashMap<>(); /** - * Marks the specified loop as fulfilling a collection obligation + * Marks the specified loop as fulfilling a collection obligation. * * @param loop the loop wrapper */ @@ -441,7 +441,7 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) /** * Utility method to get the flow-sensitive {@code CollectionOwnershipType} that the given node - * has in the given store + * has in the given store. * * @param node the node * @param coStore the store @@ -526,11 +526,11 @@ public List getCollectionFieldDestructorAnnoFields(ExecutableElement met } /** - * Determine if the given expression e refers to this.field. + * Determine if the given expression {@code e} refers to {@code this.field}. * * @param e the expression * @param field the field - * @return true if e refers to this.field + * @return true if {@code e} refers to {@code this.field} */ public boolean expressionEqualsField(String e, VariableElement field) { try { diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java index 0a7580c77f36..3c27040ba026 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java @@ -21,7 +21,7 @@ public class FenumVisitor extends BaseTypeVisitor { /** - * Creates a Fenum Visitor + * Creates a Fenum Visitor. * * @param checker the checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java index 425b842c24e8..6c1ee613b5cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java @@ -156,7 +156,7 @@ public AnnotationMirror categoriesToFormatAnnotation(I18nConversionCategory[] ar } /** - * Takes an {@code @}{@link I18nFormat} annotation, and returns its {@code value} element + * Takes an {@code @}{@link I18nFormat} annotation, and returns its {@code value} element. * * @param anno an {@code @}{@link I18nFormat} annotation * @return the {@code @}{@link I18nFormat} annotation's {@code value} element diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java index f8f7c0a16bd1..d90c1b4c6a3e 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java @@ -383,7 +383,7 @@ public static OffsetEquation createOffsetFromJavaExpression(String expressionEqu private static Pattern intPattern = Pattern.compile("[-+]?[0-9]+"); /** - * Returns true if the given string is an integer literal + * Returns true if the given string is an integer literal. * * @param string a string * @return true if the given string is an integer literal diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java index f90b10b973c9..7fbf6066a871 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java @@ -73,14 +73,14 @@ * @checker_framework.manual #lock-checker Lock Checker */ public class LockVisitor extends BaseTypeVisitor { - /** The class of GuardedBy */ + /** The class of GuardedBy. */ private static final Class checkerGuardedByClass = GuardedBy.class; - /** The class of GuardSatisfied */ + /** The class of GuardSatisfied. */ private static final Class checkerGuardSatisfiedClass = GuardSatisfied.class; - /** A pattern for spotting self receiver */ + /** A pattern for spotting self receiver. */ protected static final Pattern SELF_RECEIVER_PATTERN = Pattern.compile("^(\\.(.*))?$"); /** diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java index 746fe2ef310b..9f72fd1f40e0 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java @@ -105,7 +105,7 @@ public static List getCreatesMustCallForExpressionsAtMethodDecla } /** - * Returns the {@code CreatesMustCallFor} annotations on a method + * Returns the {@code CreatesMustCallFor} annotations on a method. * * @param method the method * @param atypeFactory the type factory to use for looking up annotations diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 4248eec553da..81b9ac6b9733 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -596,10 +596,10 @@ identifierInHeader, getNameFromExpressionTree(mit))) { } /** - * Get name from an ExpressionTree + * Get name from an ExpressionTree. * * @param expr ExpressionTree - * @return Name of the identifier the expression evaluates to or null if it doesn't + * @return name of the identifier the expression evaluates to or null if it doesn't */ protected Name getNameFromExpressionTree(ExpressionTree expr) { if (expr == null) return null; @@ -624,7 +624,7 @@ protected Name getNameFromExpressionTree(ExpressionTree expr) { * Get name from a {@code StatementTree} * * @param expr the {@code StatementTree} - * @return Name of the identifier the expression evaluates to or null if it doesn't + * @return name of the identifier the expression evaluates to or null if it doesn't */ protected Name getNameFromStatementTree(StatementTree expr) { if (expr == null) return null; @@ -639,7 +639,7 @@ protected Name getNameFromStatementTree(StatementTree expr) { } /** - * Returns the ExpressionTree of the collection in the given expression + * Returns the ExpressionTree of the collection in the given expression. * * @param expr ExpressionTree * @return the expression evaluates to or null if it doesn't diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java index ed66844a6146..22be678aaf59 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java @@ -54,7 +54,7 @@ public static enum PropagationDirection { private final KeyForPropagationReplacer replacer = new KeyForPropagationReplacer(); /** - * Creates a KeyForPropagator + * Creates a KeyForPropagator. * * @param unknownKeyfor an {@link UnknownKeyFor} annotation */ diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java index 326cf0495494..8edcc975ef70 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java @@ -155,7 +155,7 @@ public TransferResult visitGreaterThanOrEqual( /** * If possibleMatcher is a call of groupCount on a Matcher and possibleConstant is a constant, - * annotate the matcher as constant + 1 if !isAlsoEqual constant if isAlsoEqual + * annotate the matcher as constant + 1 if !isAlsoEqual constant if isAlsoEqual. * * @param possibleMatcher the Node that might be a call of Matcher.groupCount() * @param possibleConstant the Node that might be a constant diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index fd739eb71c18..ea0d5348f8e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -530,7 +530,7 @@ public CollectionObligation( } /** - * Create a CollectionObligation from an Obligation + * Create a CollectionObligation from an Obligation. * * @param obligation the obligation to create a CollectionObligation from * @param mustCallMethod the method that must be called on the elements of the collection @@ -779,7 +779,8 @@ private void addObligationsForOwningCollectionReturn(Set obligations coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree()); if (mustCallValues == null) { throw new BugInCF( - "List of MustCall values of component type is null for OwningCollection return value: " + "List of MustCall values of component type is null for OwningCollection return value:" + + " " + node); } if (!ResourceLeakUtils.isIterator(node.getType())) { @@ -871,7 +872,7 @@ private void checkOwningResourceCollectionFieldAccess(FieldAccessNode node) { * * @param obligations the Obligations to update * @param node the method or constructor invocation - * @param exceptionType a description of the outgoing CFG edge from the node: null to + * @param exceptionType a description of the outgoing CFG edge from the node: {@code null} to * indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given * throwable class was thrown */ @@ -1260,7 +1261,7 @@ private boolean returnTypeIsMustCallAliasWithUntrackable(MethodInvocationNode no * @param obligations the current set of Obligations, which is side-effected to remove Obligations * for locals that are passed as owning parameters to the method or constructor * @param node a method or constructor invocation node - * @param exceptionType a description of the outgoing CFG edge from the node: null to + * @param exceptionType a description of the outgoing CFG edge from the node: {@code null} to * indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given * throwable class was thrown */ @@ -2488,9 +2489,8 @@ private void propagateObligationsToSuccessorBlocks( * @param obligations the Obligations for the current block * @param currentBlock the current block * @param successor a successor of the current block - * @param exceptionType the type of edge from currentBlock to successor - * : null for normal control flow, or a throwable type for exceptional - * control flow + * @param exceptionType the type of edge from {@code currentBlock} to {@code successor}: {@code + * null} for normal control flow, or a throwable type for exceptional control flow * @param visited block-Obligations pairs already analyzed or already on the worklist * @param worklist current worklist * @throws InvalidLoopBodyAnalysisException if the propagation is called as part of a loop body @@ -3644,7 +3644,7 @@ private List getCalledMethods(AccumulationValue cmVal) { private static class InvalidLoopBodyAnalysisException extends Exception { /** - * Construct an InvalidLoopBodyAnalysisException + * Construct an InvalidLoopBodyAnalysisException. * * @param message the error message */ diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 723c1ff522b0..5bbdcaf50656 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -323,7 +323,7 @@ public boolean isTempVar(Node node) { } /** - * Gets the tree for a temporary variable + * Gets the tree for a temporary variable. * * @param node a node for a temporary variable * @return the tree for {@code node} diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java index b0a3f03a3a3e..041a07c75a73 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsChecker.java @@ -25,7 +25,7 @@ public class RLCCalledMethodsChecker extends CalledMethodsChecker { /** Creates a RLCCalledMethodsChecker. */ public RLCCalledMethodsChecker() {} - /** The parent resource leak checker */ + /** The parent resource leak checker. */ private @MonotonicNonNull ResourceLeakChecker rlc; @Override @@ -79,7 +79,10 @@ private ResourceLeakChecker getResourceLeakChecker() { this.rlc = ResourceLeakUtils.getResourceLeakChecker(this); } catch (TypeSystemError e) { throw new UserError( - "Cannot find ResourceLeakChecker in checker hierarchy. The RLCCalledMethods checker shouldn't be invoked directly, it is only a subchecker of the ResourceLeakChecker. Use the ResourceLeakChecker or the CalledMethodsChecker instead."); + "Cannot find ResourceLeakChecker in checker hierarchy. The RLCCalledMethods checker" + + " shouldn't be invoked directly, it is only a subchecker of the" + + " ResourceLeakChecker. Use the ResourceLeakChecker or the CalledMethodsChecker" + + " instead."); } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java index 9bb2c80ddf67..d1c07bf128e5 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java @@ -133,7 +133,7 @@ private void checkCreatesMustCallForTargetsHaveNonEmptyMustCall( } /** - * Check that an overriding method does not reduce the number of created must-call obligations + * Check that an overriding method does not reduce the number of created must-call obligations. * * @param tree overriding method * @param elt element for overriding method @@ -589,11 +589,11 @@ private void checkOwningField(VariableElement field) { } /** - * Determine if the given expression e refers to this.field. + * Determine if the given expression {@code e} refers to {@code this.field}. * * @param e the expression * @param field the field - * @return true if e refers to this.field + * @return true if {@code e} refers to {@code this.field} */ private boolean expressionEqualsField(String e, VariableElement field) { try { diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java index 91711b477fb6..14dbcf8ca5eb 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java @@ -81,7 +81,7 @@ private static boolean isLiteral(ExpressionTree expr) { } /** - * Returns the long value of an Integer or a Long + * Returns the long value of an Integer or a Long. * * @param obj either an Integer or a Long * @return the long value of obj diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java index 8e39721c3dfd..84a52d32d820 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java @@ -29,15 +29,15 @@ public class UnitsRelationsDefault implements UnitsRelations { @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method protected AnnotationMirror m, km, mm, s, g, kg; - /** Derived SI units without special names */ + /** Derived SI units without special names. */ @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method protected AnnotationMirror m2, km2, mm2, m3, km3, mm3, mPERs, mPERs2; - /** Derived SI units with special names */ + /** Derived SI units with special names. */ @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method protected AnnotationMirror N, kN; - /** Non-SI units */ + /** Non-SI units. */ @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method protected AnnotationMirror h, kmPERh, t; diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java index 611cbfea5e32..672cfcf7ccf8 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NonEmptyTest.java @@ -5,7 +5,7 @@ import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -/** JUnit tests for the Non-Empty Checker */ +/** JUnit tests for the Non-Empty Checker. */ public class NonEmptyTest extends CheckerFrameworkPerDirectoryTest { /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java index 9c5bc2e2b0d8..730e870ed1fb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java @@ -11,7 +11,7 @@ */ public final class UnusedAbstractValue implements AbstractValue { - /** This class cannot be instantiated */ + /** This class cannot be instantiated. */ private UnusedAbstractValue() { throw new AssertionError("Class UnusedAbstractValue cannot be instantiated."); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java index 314b062d5601..87bb87f66a38 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java @@ -71,7 +71,7 @@ public void putBusyExpr(BusyExprValue e) { } /** - * Add expressions to the store, add sub-expressions to the store recursively + * Add expressions to the store, add sub-expressions to the store recursively. * * @param e the expression to be added */ diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java index 45c0e30ccfb4..0e2c0143a6ef 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java @@ -14,7 +14,7 @@ import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.cfg.node.ReturnNode; -/** A busy expression transfer function */ +/** A busy expression transfer function. */ public class BusyExprTransfer extends AbstractNodeVisitor< TransferResult, diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 1bc60a9d974d..6847eecf1b43 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -607,7 +607,7 @@ public Node visitYield17(Tree yieldTree, Void p) { } /** - * Visit a SwitchExpressionTree + * Visit a SwitchExpressionTree. * * @param switchExpressionTree a SwitchExpressionTree, typed as Tree to be backward-compatible * @param p parameter @@ -622,7 +622,7 @@ public Node visitSwitchExpression17(Tree switchExpressionTree, Void p) { } /** - * Visit a BindingPatternTree + * Visit a BindingPatternTree. * * @param bindingPatternTree a BindingPatternTree, typed as Tree to be backward-compatible * @param p parameter diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java index f6fae7e9e7aa..1617a5074b39 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java @@ -47,7 +47,7 @@ public class AssignmentNode extends Node { /** The node for the RHS of the assignment tree. */ protected final Node rhs; - /** True if the assignment node is synthetic */ + /** True if the assignment node is synthetic. */ protected final boolean synthetic; /** True if the assignment node is desugared from an enhanced-for-loop over an array. */ diff --git a/framework/src/main/java/org/checkerframework/common/util/count/report/ReportChecker.java b/framework/src/main/java/org/checkerframework/common/util/count/report/ReportChecker.java index f8058381704d..38abb1f4c504 100644 --- a/framework/src/main/java/org/checkerframework/common/util/count/report/ReportChecker.java +++ b/framework/src/main/java/org/checkerframework/common/util/count/report/ReportChecker.java @@ -24,9 +24,9 @@ *

Options: * *

    - *
  • -AreportTreeKinds: comma-separated list of Tree.Kinds that should - * be reported - *
  • -AreportModifiers: comma-separated list of modifiers that should be reported + *
  • {@code -AreportTreeKinds}: comma-separated list of {@code Tree.Kind}s that should be + * reported + *
  • {@code -AreportModifiers}: comma-separated list of modifiers that should be reported *
* * @see org.checkerframework.common.util.count.AnnotationStatistics From 6b0b3275315722d25c03610e975d4d60bc0a17d2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 09:10:54 -0700 Subject: [PATCH 253/374] Rename variable --- ...ollectionOwnershipAnnotatedTypeFactory.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 86d151745196..bd345a2e7781 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -244,15 +244,13 @@ public void postAnalyze(ControlFlowGraph cfg) { *

This overload should be used before computation of AnnotatedTypeMirrors is completed, in * particular in addComputedTypeAnnotations(AnnotatedTypeMirror). * - *

That is, whether the given type is - * * @param t the AnnotatedTypeMirror * @return true if t is a resource collection */ public boolean isResourceCollection(TypeMirror t) { if (t == null) return false; - List list = getMustCallValuesOfResourceCollectionComponent(t); - return list != null && list.size() > 0; + List mcValues = getMustCallValuesOfResourceCollectionComponent(t); + return mcValues != null && mcValues.size() > 0; } /** @@ -265,7 +263,9 @@ public boolean isResourceCollection(TypeMirror t) { * declaration */ public boolean isOwningCollectionField(Element elt) { - if (elt == null) return false; + if (elt == null) { + return false; + } if (isResourceCollection(elt.asType())) { if (elt.getKind().isField()) { AnnotatedTypeMirror atm = getAnnotatedType(elt); @@ -293,7 +293,9 @@ public boolean isOwningCollectionField(Element elt) { * @return true if the element is a resource collection field. */ public boolean isResourceCollectionField(Element elt) { - if (elt == null) return false; + if (elt == null) { + return false; + } if (isResourceCollection(elt.asType())) { if (elt.getKind().isField()) { return true; @@ -312,7 +314,9 @@ public boolean isResourceCollectionField(Element elt) { * {@code @OwningCollection} by declaration */ public boolean isOwningCollectionParameter(Element elt) { - if (elt == null) return false; + if (elt == null) { + return false; + } if (isResourceCollection(elt.asType())) { if (elt.getKind() == ElementKind.PARAMETER) { AnnotatedTypeMirror atm = getAnnotatedType(elt); From a012a9d6eab1ed3ed554c0615c61919cf2c126b1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 09:13:39 -0700 Subject: [PATCH 254/374] Use curly braces around then and else clauses --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index bd345a2e7781..81d34f6f66ae 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -248,7 +248,9 @@ public void postAnalyze(ControlFlowGraph cfg) { * @return true if t is a resource collection */ public boolean isResourceCollection(TypeMirror t) { - if (t == null) return false; + if (t == null) { + return false; + } List mcValues = getMustCallValuesOfResourceCollectionComponent(t); return mcValues != null && mcValues.size() > 0; } From 96bd8a1326fdb08ae2223bd2f1b5ec432efe559b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Fri, 25 Jul 2025 09:29:09 -0700 Subject: [PATCH 255/374] Documentation tweaks and improve efficiency by doing cheap test first --- ...llectionOwnershipAnnotatedTypeFactory.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 81d34f6f66ae..54a45e5d4bcd 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -256,11 +256,11 @@ public boolean isResourceCollection(TypeMirror t) { } /** - * Returns true if the given element is a resource collection field that is - * {@code @OwningCollection} by declaration, which is the default behavior, i.e. with no different - * collection ownership annotation. + * Returns true if the given element is a resource collection field that is declared as + * {@code @OwningCollection}. Since that is the default, this method also returns true if the + * field has no collection ownership annotation. * - * @param elt the element + * @param elt a field that might be a resource collection * @return true if the element is a resource collection field that is {@code @OwningCollection} by * declaration */ @@ -268,8 +268,8 @@ public boolean isOwningCollectionField(Element elt) { if (elt == null) { return false; } - if (isResourceCollection(elt.asType())) { - if (elt.getKind().isField()) { + if (elt.getKind().isField()) { + if (isResourceCollection(elt.asType())) { AnnotatedTypeMirror atm = getAnnotatedType(elt); CollectionOwnershipType fieldType = getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); @@ -291,15 +291,15 @@ public boolean isOwningCollectionField(Element elt) { /** * Returns true if the given element is a resource collection field. * - * @param elt the element - * @return true if the element is a resource collection field. + * @param elt an element + * @return true if the element is a resource collection field */ public boolean isResourceCollectionField(Element elt) { if (elt == null) { return false; } - if (isResourceCollection(elt.asType())) { - if (elt.getKind().isField()) { + if (elt.getKind().isField()) { + if (isResourceCollection(elt.asType())) { return true; } } @@ -307,11 +307,11 @@ public boolean isResourceCollectionField(Element elt) { } /** - * Returns true if the given element is a resource collection parameter that is - * {@code @OwningCollection} by declaration, which is the default behavior, i.e. with no different - * collection ownership annotation. + * Returns true if the given element is a resource collection parameter that is declared as + * {@code @OwningCollection}. Since that is the default, this method also returns true if the + * parameter has no collection ownership annotation. * - * @param elt the element + * @param elt an element * @return true if the element is a resource collection parameter that is * {@code @OwningCollection} by declaration */ @@ -356,8 +356,8 @@ public boolean isResourceCollection(Tree tree) { } catch (BugInCF e) { return false; } - List list = getMustCallValuesOfResourceCollectionComponent(treeMcType); - return list != null && list.size() > 0; + List mcValues = getMustCallValuesOfResourceCollectionComponent(treeMcType); + return mcValues != null && mcValues.size() > 0; } /** @@ -397,7 +397,7 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType /** * If the given tree represents a collection, this method returns the MustCall values of its - * elements or null if there are none or if the given type is not a collection. + * elements. It returns null if there are none or if the given type is not a collection. * *

That is, if the given tree is of a Java.util.Collection implementation, this method returns * the MustCall values of its type variable upper bound if there are any or else null. @@ -412,8 +412,8 @@ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { } /** - * If the given type is a collection, this method returns the MustCall values of its elements or - * null if there are none or if the given type is not a collection. + * If the given type is a collection, this method returns the MustCall values of its elements. It + * returns null if there are none or if the given type is not a collection. * *

That is, if the given type is a Java.util.Collection implementation, this method returns the * MustCall values of its type variable upper bound if there are any or else null. From 5d559580c527828b8f1641a408d7b2beb2de4858 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 27 Jul 2025 10:29:29 -0700 Subject: [PATCH 256/374] Improve Javadoc style --- .../dataflow/qual/AssertMethod.java | 8 ++++---- .../CollectionOwnershipAnnotatedTypeFactory.java | 14 +++++++------- .../InitializationAnnotatedTypeFactory.java | 4 ++-- .../initialization/InitializationVisitor.java | 2 +- .../mustcall/MustCallAnnotatedTypeFactory.java | 4 ++-- .../checker/mustcall/MustCallVisitor.java | 4 ++-- .../checker/regex/RegexTransfer.java | 2 +- .../MustCallConsistencyAnalyzer.java | 7 +++---- .../checker/resourceleak/ResourceLeakUtils.java | 12 ++++++------ .../RLCCalledMethodsAnnotatedTypeFactory.java | 16 ++++++++-------- .../RLCCalledMethodsTransfer.java | 5 ++--- .../cfg/visualize/AbstractCFGVisualizer.java | 2 +- .../expression/IteratedCollectionElement.java | 2 +- .../common/accumulation/AccumulationStore.java | 4 ++-- .../common/basetype/BaseTypeVisitor.java | 4 ++-- .../framework/flow/CFAbstractStore.java | 2 +- .../framework/flow/CFAbstractTransfer.java | 16 ++++++++-------- .../framework/flow/CFAbstractValue.java | 4 ++-- .../type/GenericAnnotatedTypeFactory.java | 7 ++++--- 19 files changed, 59 insertions(+), 60 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java index 5dadfe292c5c..6f8319e2c434 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java @@ -14,9 +14,9 @@ *

The annotation enables flow-sensitive type refinement to be more precise. For example, if * {@code Assertions.assertTrue} is annotated as follows: * - *

@AssertMethod(value = AssertionFailedError.class)
+ * 
{@code @AssertMethod(value = AssertionFailedError.class)
  * public static void assertFalse(boolean condition);
- * 
+ * }
* * Then, in the code below, the Optional Checker can determine that {@code optional} has a value and * the call to {@code Optional#get} will not throw an exception. @@ -59,9 +59,9 @@ * href="https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html#assertFalse-boolean-">Assertions.assertFalse(...) * throws an exception if the first argument is false. So it is annotated as follows: * - *
@AssertMethod(value = AssertionFailedError.class, isAssertFalse = true)
+   * 
{@code @AssertMethod(value = AssertionFailedError.class, isAssertFalse = true)
    * public static void assertFalse(boolean condition);
-   * 
+ * }
* * @return the value for {@link #parameter} on which the method throws an exception */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 54a45e5d4bcd..8d73365c5ab8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -369,7 +369,7 @@ public boolean isResourceCollection(Tree tree) { * * @param atm the AnnotatedTypeMirror * @return if the given type is a collection, returns the MustCall values of its elements or null - * if there are none or if the given type is not a collection. + * if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedTypeMirror atm) { if (atm == null) { @@ -404,7 +404,7 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType * * @param tree the AST tree * @return if the given tree represents a collection, returns the MustCall values of its elements - * or null if there are none or if the given type is not a collection. + * or null if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); @@ -420,7 +420,7 @@ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { * * @param t the TypeMirror * @return if the given type is a collection, returns the MustCall values of its elements or null - * if there are none or if the given type is not a collection. + * if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) { if (t == null) { @@ -451,7 +451,7 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) * * @param node the node * @param coStore the store - * @return the {@code CollectionOwnershipType} that the given node has in the given store. + * @return the {@code CollectionOwnershipType} that the given node has in the given store */ public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coStore) { try { @@ -468,7 +468,7 @@ public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coS * has. * * @param tree the tree - * @return the {@code CollectionOwnershipType} that the given tree has. + * @return the {@code CollectionOwnershipType} that the given tree has */ public CollectionOwnershipType getCoType(Tree tree) { JavaExpression jx = null; @@ -518,7 +518,7 @@ public CollectionOwnershipType getCoType(Collection annos) { * * @param method the method * @return the field names in the {@code @CollectionFieldDestructor} annotation that the given - * method has or an empty list if there is no such annotation. + * method has or an empty list if there is no such annotation */ public List getCollectionFieldDestructorAnnoFields(ExecutableElement method) { AnnotationMirror collectionFieldDestructorAnno = @@ -550,7 +550,7 @@ public boolean expressionEqualsField(String e, VariableElement field) { } /** - * Return a JavaExpression for the given String or null if the conversion fails. + * Returns a JavaExpression for the given String or null if the conversion fails. * * @param s the string * @param method the method with the annotation diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java index 7bd5e11c5f91..40292b017f85 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java @@ -571,7 +571,7 @@ protected AnnotationMirror getUnderInitializationAnnotationOfSuperType(TypeMirro * * @param store a store * @param path the current path, used to determine the current class - * @param isStatic whether to report static fields or instance fields + * @param isStatic if true, report static fields or instance fields * @param receiverAnnotations the annotations on the receiver * @return the fields that are not yet initialized in a given store (a pair of lists) */ @@ -610,7 +610,7 @@ public IPair, List> getUninitializedFields( * * @param store a store * @param path the current path, used to determine the current class - * @param isStatic whether to report static fields or instance fields + * @param isStatic if true, report static fields or instance fields * @param receiverAnnotations the annotations on the receiver * @return the fields that have the invariant annotation and are not yet initialized in a given * store (a pair of lists) diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index 5cdade6e2df0..a0411045555f 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -320,7 +320,7 @@ private List getAllReceiverAnnotations(MethodTree tr * @param tree a {@link ClassTree} if {@code staticFields} is true; a {@link MethodTree} for a * constructor if {@code staticFields} is false. This is where errors are reported, if they * are not reported at the fields themselves - * @param staticFields whether to check static fields or instance fields + * @param staticFields if true, check static fields or instance fields * @param store the store * @param receiverAnnotations the annotations on the receiver */ diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index e037a7b37da3..229f794246cd 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -171,7 +171,7 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { * * @param tree the tree * @param adt the annotated declared type for which all Collection type variables with Top type - * are to be replaced with Bottom. + * are to be replaced with Bottom */ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclaredType adt) { if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) { @@ -589,7 +589,7 @@ protected QualifierHierarchy createQualifierHierarchy() { * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code * afterFirstStore} is false, the store before {@code succBlock} is returned. * - * @param afterFirstStore whether to use the store after the first block or the store before its + * @param afterFirstStore if true, use the store after the first block or the store before its * successor, {@code succBlock} * @param firstBlock a CFG block * @param succBlock {@code firstBlock}'s successor diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 81b9ac6b9733..0d2a0b49263b 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -596,7 +596,7 @@ identifierInHeader, getNameFromExpressionTree(mit))) { } /** - * Get name from an ExpressionTree. + * Returns the identifier of an ExpressionTree, or null. * * @param expr ExpressionTree * @return name of the identifier the expression evaluates to or null if it doesn't @@ -621,7 +621,7 @@ protected Name getNameFromExpressionTree(ExpressionTree expr) { } /** - * Get name from a {@code StatementTree} + * Returns the name from a {@code StatementTree}, or null. * * @param expr the {@code StatementTree} * @return name of the identifier the expression evaluates to or null if it doesn't diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java index 8edcc975ef70..efb5fa61f8ab 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java @@ -159,7 +159,7 @@ public TransferResult visitGreaterThanOrEqual( * * @param possibleMatcher the Node that might be a call of Matcher.groupCount() * @param possibleConstant the Node that might be a constant - * @param isAlsoEqual whether the comparison operation is strict or reflexive + * @param isAlsoEqual if true, the comparison operation is strict; if false, it is reflexive * @param resultIn the TransferResult * @return the possibly refined output TransferResult */ diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 1c2869081892..08591e635385 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -517,7 +517,7 @@ static class CollectionObligation extends Obligation { /** * Create a CollectionObligation from a set of resource aliases. * - * @param mustCallMethod the method to be called on all elements of the collection. + * @param mustCallMethod the method to be called on all elements of the collection * @param resourceAliases a set of resource aliases * @param whenToEnforce when this Obligation should be enforced */ @@ -3293,8 +3293,7 @@ public void findFulfillingForEachLoops(ControlFlowGraph cfg) { } /** - * Checks whether the given {@code MethodInvocationNode} is desugared from an enhanced for loop - * and calls a loop-body-analysis on the detected loop if it is. + * Calls a loop-body-analysis on the loop if it is desugared from an enhanced for loop. * *

If a {@code MethodInvocationNode} is desugared from an enhanced for loop over a collection * it corresponds to the node in the synthetic {@code Iterator.next()} method call, which is the @@ -3308,7 +3307,7 @@ public void findFulfillingForEachLoops(ControlFlowGraph cfg) { * MustCallOnElements checker. * * @param methodInvocationNode the {@code MethodInvocationNode}, for which it is checked, whether - * it is desugared from an enhanced for loop. + * it is desugared from an enhanced for loop * @param cfg the enclosing cfg of the {@code MethodInvocationNode} */ private void patternMatchEnhancedCollectionForLoop( diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 2239c4568d6a..d117d54be815 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -268,7 +268,7 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno * * @param element the element * @param atf an AnnotatedTypeFactory to get the annotated type of the element - * @return whether the given element is a Java.lang.Iterable or Java.util.Iterator type + * @return true if the given element is a Java.lang.Iterable or Java.util.Iterator type */ public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { if (element == null) return false; @@ -283,7 +283,7 @@ public static boolean isCollection(Element element, AnnotatedTypeFactory atf) { * assignable from either. * * @param type the TypeMirror - * @return whether type is a java.lang.Iterable or java.util.Iterator + * @return true if type is a java.lang.Iterable or java.util.Iterator */ public static boolean isCollection(TypeMirror type) { if (type == null) return false; @@ -341,7 +341,7 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem * @param type the {@code TypeMirror} * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type * @return the list of mustcall obligations for the upper bound of {@code type} or null if the - * upper bound is null. + * upper bound is null */ public static @Nullable List getMcValues( TypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { @@ -398,7 +398,7 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem * * @param type the {@code AnnotatedTypeMirror} * @param mcAtf the {@code MustCallAnnotatedTypeFactory} to get the {@code MustCall} type - * @return the list of mustcall obligations for the upper bound of {@code type}. + * @return the list of mustcall obligations for the upper bound of {@code type} */ public static List getMcValues( AnnotatedTypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { @@ -413,7 +413,7 @@ public static List getMcValues( } /** - * Return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation. + * Returns true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation. * * @param typeMirror the {@code TypeMirror} * @return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation @@ -429,7 +429,7 @@ public static boolean hasManualMustCallUnknownAnno(TypeMirror typeMirror) { } /** - * Return true if the passed {@code AnnotatedTypeMirror} has a manual {@code MustCallUnknown} + * Returns true if the passed {@code AnnotatedTypeMirror} has a manual {@code MustCallUnknown} * annotation. * * @param annotatedTypeMirror the {@code AnnotatedTypeMirror} diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 5bbdcaf50656..6baf9ecd89cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -150,9 +150,9 @@ public static void addPotentiallyFulfillingLoop( } /** - * Return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. + * Returns the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. * - * @return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. + * @return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis */ public static Set getPotentiallyFulfillingLoops() { return potentiallyFulfillingLoops; @@ -664,7 +664,7 @@ public static enum LoopKind { * @param collectionElementTree AST {@code Tree} for collection element iterated over * @param condition AST {@code Tree} for loop condition * @param associatedMethods set of methods associated with this loop - * @param loopKind whether this is an assigning/fulfilling loop + * @param loopKind the type of loop, e.g., assigning/fulfilling */ protected CollectionObligationAlteringLoop( ExpressionTree collectionTree, @@ -695,7 +695,7 @@ public void addMethods(Set methods) { } /** - * Return methods associated with this loop. For assigning loops, these are methods that are to + * Returns methods associated with this loop. For assigning loops, these are methods that are to * be added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to * be removed from the {@code MustCallOnElements} and added to the {@code * CalledMethodsOnElements} type. @@ -710,16 +710,16 @@ public Set getMethods() { /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ public static class PotentiallyFulfillingLoop extends CollectionObligationAlteringLoop { - /** cfg {@code Block} containing the loop body entry */ + /** cfg {@code Block} containing the loop body entry. */ public final Block loopBodyEntryBlock; - /** cfg {@code Block} containing the loop update */ + /** cfg {@code Block} containing the loop update. */ public final Block loopUpdateBlock; - /** cfg conditional {@link Block} following loop condition */ + /** cfg conditional {@link Block} following loop condition. */ public final ConditionalBlock loopConditionalBlock; - /** cfg {@code Node} for the collection element iterated over */ + /** cfg {@code Node} for the collection element iterated over. */ public final Node collectionElementNode; /** diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 9f83782a2c37..4cd96fb55595 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -79,9 +79,8 @@ public AccumulationStore initialStore( } /** - * Checks whether the given node is the element of a collection iterated over in a - * potentially-Mcoe-obligation-fulfilling loop. If it is, accumulates the called methods to this - * collection element. + * Accumulates the called methods to this collection element if the given node is the element of a + * collection iterated over in a potentially-Mcoe-obligation-fulfilling loop. * * @param valuesAsList the list of called methods * @param result the transfer result diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java index b854c987d057..d50e569c9024 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java @@ -293,7 +293,7 @@ public String visualizeBlockNode(Node n, @Nullable Analysis analysis) { return sbBlockNode.toString(); } - /** Whether to visualize before or after a block. */ + /** Where to visualize: before or after a block. */ protected enum VisualizeWhere { /** Visualize before the block. */ BEFORE, diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index 7e0f5bc18575..85593588349f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -7,7 +7,7 @@ /** * Represents a collection element that is iterated over in a potentially - * collection-obligation-fulfilling loop, for example {@code o} in {@code for (Object o: list) { }} + * collection-obligation-fulfilling loop, for example {@code o} in {@code for (Object o: list) { }}. */ public class IteratedCollectionElement extends JavaExpression { /** The CFG node for this collection element. */ diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java index f05d01697182..5bada35c2633 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java @@ -10,8 +10,8 @@ public class AccumulationStore extends CFAbstractStore analysis, diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index f90656cab2e9..86b17966947a 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -3307,7 +3307,7 @@ protected final void commonAssignmentCheckStartDiagnostic( * Prints a diagnostic about exiting {@code commonAssignmentCheck()}, if the showchecks option was * set. * - * @param success whether the check succeeded or failed + * @param success true if the check succeeded * @param extraMessage information about why the result is what it is; may be null * @param varType the annotated type of the variable * @param valueType the annotated type of the value @@ -4505,7 +4505,7 @@ private boolean checkReturn() { /** * Issue an error message or log message about checking an overriding return type. * - * @param success whether the check succeeded or failed + * @param success true if the check succeeded */ private void checkReturnMsg(boolean success) { if (success && !showchecks) { diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index cda9d339bf10..586e5e79c6c4 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -808,7 +808,7 @@ public void clearValue(JavaExpression expr) { * * @param node CFG node for which the associated {@link IteratedCollectionElement} is sought * @param tree AST tree for which the associated {@link IteratedCollectionElement} is sought - * @return the {@link IteratedCollectionElement} associated with the given node or tree. + * @return the {@link IteratedCollectionElement} associated with the given node or tree */ @SuppressWarnings("interning:not.interned") // we want to check reference equality public @Nullable IteratedCollectionElement getIteratedCollectionElement(Node node, Tree tree) { diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java index 577bd4411c8e..981ff59908ea 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -408,15 +408,15 @@ public S initialStore(UnderlyingAST underlyingAST, List param * *

For example, given the following code: * - *


-   *   void operateOver(Container container) {
-   *      container.forEach(item -> {...});
-   *   }
+   * 
{@code
+   * void operateOver(Container container) {
+   *    container.forEach(item -> {...});
+   * }
    *
-   *   class Container {
-   *     void forEach(@NonLeaked Consumer<T>)
-   *   }
-   * 
+ * class Container { + * void forEach(@NonLeaked Consumer) + * } + * }
* * The lambda passed to {@code Container.forEach} is not leaked, as the parameter is annotated * with @{@link NonLeaked}. diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java index 758469511627..d471e9f64321 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java @@ -817,7 +817,7 @@ protected AnnotationMirrorSet combineSets( * @param aAtv a type variable that does not have a primary annotation in {@code top} hierarchy * @param bAtv a type variable that does not have a primary annotation in {@code top} hierarchy * @param top the top annotation in the hierarchy - * @param canCombinedSetBeMissingAnnos whether or not TODO + * @param canCombinedSetBeMissingAnnos true if TODO * @return the result of combining the two type variables, which may be null */ protected abstract @Nullable AnnotationMirror combineTwoTypeVars( @@ -834,7 +834,7 @@ protected AnnotationMirrorSet combineSets( * @param annotation an annotation * @param typeVar a type variable that does not have a primary annotation in the hierarchy * @param top the top annotation of the hierarchy - * @param canCombinedSetBeMissingAnnos whether or not TODO + * @param canCombinedSetBeMissingAnnos true if TODO * @return the result of combining {@code annotation} with {@code typeVar} */ protected abstract @Nullable AnnotationMirror combineAnnotationWithTypeVar( 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 6ca103817af8..988bf2974668 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1219,7 +1219,7 @@ public Store getStoreBefore(Set nodes) { * Returns the regular store for a given block. * * @param block a block whose regular store to return - * @return the regular store of {@code block}. + * @return the regular store of {@code block} */ public Store getRegularStore(Block block) { TransferInput input; @@ -2882,7 +2882,7 @@ public final List getPostconditionAnnotations( * of {@code preOrPost}) * @param declaredType the declared type of the expression, which is used to determine if the * inferred type supplies no additional information beyond the declared type - * @param preOrPost whether to return preconditions or postconditions + * @param preOrPost if true, return preconditions; if false, return postconditions * @param preconds the precondition annotations for the method; used to suppress redundant * postconditions; non-null exactly when {@code preOrPost} is {@code AFTER} * @return precondition or postcondition annotations for the element (possibly an empty list) @@ -2948,7 +2948,8 @@ protected List getPreOrPostconditionAnnotations( * @param qualifier the qualifier that must be present * @param declaredType the declared type of the expression, which is used to avoid inferring * redundant pre- or postcondition annotations - * @param preOrPost whether to return a precondition or postcondition annotation + * @param preOrPost if true, return a precondition annotation; if false, return a postcondition + * annotation * @param preconds the list of precondition annotations; used to suppress redundant * postconditions; non-null exactly when {@code preOrPost} is {@code BeforeOrAfter.BEFORE} * @return a {@code RequiresQualifier("...")} or {@code EnsuresQualifier("...")} annotation for From 7db0254ea45894f43cfe6b8a646a8a2e39c1ceb2 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 27 Jul 2025 13:26:09 -0700 Subject: [PATCH 257/374] Checkpoint --- ...llectionOwnershipAnnotatedTypeFactory.java | 23 ++++++++++--------- .../CollectionOwnershipVisitor.java | 2 +- .../RLCCalledMethodsVisitor.java | 6 ++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 862c3c923d8f..0945ccae6a7a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -516,28 +516,27 @@ public CollectionOwnershipType getCoType(Collection annos) { * method has or an empty list if there is no such annotation. * * @param method the method - * @return the field names in the {@code @CollectionFieldDestructor} annotation that the given - * method has or an empty list if there is no such annotation + * @return the field names in the method's {@code @CollectionFieldDestructor} annotation, or an + * empty list if there is no such annotation */ public List getCollectionFieldDestructorAnnoFields(ExecutableElement method) { AnnotationMirror collectionFieldDestructorAnno = getDeclAnnotation(method, CollectionFieldDestructor.class); - if (collectionFieldDestructorAnno != null) { - return AnnotationUtils.getElementValueArray( - collectionFieldDestructorAnno, collectionFieldDestructorValueElement, String.class); - } else { + if (collectionFieldDestructorAnno == null) { return new ArrayList(); } + return AnnotationUtils.getElementValueArray( + collectionFieldDestructorAnno, collectionFieldDestructorValueElement, String.class); } /** - * Determine if the given expression {@code e} refers to {@code this.field}. + * Returnst true if the given expression {@code e} refers to {@code this.field}. * * @param e the expression * @param field the field * @return true if {@code e} refers to {@code this.field} */ - public boolean expressionEqualsField(String e, VariableElement field) { + public boolean expressionIsFieldAccess(String e, VariableElement field) { try { JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); @@ -549,15 +548,17 @@ public boolean expressionEqualsField(String e, VariableElement field) { } /** - * Returns a JavaExpression for the given String or null if the conversion fails. + * Returns a JavaExpression for the given String. Returns null if string is not parsable as a Java + * expression. * * @param s the string * @param method the method with the annotation - * @return a JavaExpression for the given String or null if the conversion fails + * @return a JavaExpression for the given String, or null if the string is not parsable as a Java + * expression */ public JavaExpression stringToJavaExpression(String s, ExecutableElement method) { Tree methodTree = declarationFromElement(method); - if (methodTree != null && (methodTree instanceof MethodTree)) { + if (methodTree instanceof MethodTree) { try { return StringToJavaExpression.atMethodBody(s, (MethodTree) methodTree, checker); } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index def5769ba88e..1c830305cfc7 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -145,7 +145,7 @@ private void checkOwningCollectionField(VariableTree fieldTree) { List destructedFields = atypeFactory.getCollectionFieldDestructorAnnoFields(siblingMethod); for (String destructedFieldName : destructedFields) { - if (atypeFactory.expressionEqualsField(destructedFieldName, fieldElement)) { + if (atypeFactory.expressionIsFieldAccess(destructedFieldName, fieldElement)) { return; } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java index 367f400d6207..75c0521aa658 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java @@ -528,7 +528,7 @@ private void checkOwningField(VariableElement field) { rlTypeFactory.ensuresCalledMethodsValueElement, String.class); for (String value : values) { - if (expressionEqualsField(value, field)) { + if (expressionIsFieldAccess(value, field)) { List methods = AnnotationUtils.getElementValueArray( ensuresCalledMethodsAnno, @@ -545,7 +545,7 @@ private void checkOwningField(VariableElement field) { Set exceptionalPostconds = rlTypeFactory.getExceptionalPostconditions(siblingMethod); for (EnsuresCalledMethodOnExceptionContract postcond : exceptionalPostconds) { - if (expressionEqualsField(postcond.getExpression(), field)) { + if (expressionIsFieldAccess(postcond.getExpression(), field)) { unsatisfiedMustCallObligationsOfOwningField.remove( new DestructorObligation( postcond.getMethod(), @@ -595,7 +595,7 @@ private void checkOwningField(VariableElement field) { * @param field the field * @return true if {@code e} refers to {@code this.field} */ - private boolean expressionEqualsField(String e, VariableElement field) { + private boolean expressionIsFieldAccess(String e, VariableElement field) { try { JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); From 83fed79ebf749bbce5306faea58d45e9def5152c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 10:15:34 -0700 Subject: [PATCH 258/374] Variable names --- .../CalledMethodsAnnotatedTypeFactory.java | 2 +- ...llectionOwnershipAnnotatedTypeFactory.java | 22 +++++++++---------- .../accumulation/AccumulationStore.java | 4 ++-- .../accumulation/AccumulationVisitor.java | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java index f5d333bede68..c0e7786ba1a9 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java @@ -340,7 +340,7 @@ public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror type) { private class CalledMethodsTypeAnnotator extends TypeAnnotator { /** - * Constructor matching super. + * Creates a CalledMethodsTypeAnnotator. * * @param atypeFactory the type factory */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 0945ccae6a7a..a5dc25184d16 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -583,7 +583,7 @@ protected TypeAnnotator createTypeAnnotator() { private class CollectionOwnershipTypeAnnotator extends TypeAnnotator { /** - * Constructor matching super. + * Creates a CollectionOwnershipTypeAnnotator. * * @param atypeFactory the type factory */ @@ -593,11 +593,10 @@ public CollectionOwnershipTypeAnnotator(AnnotatedTypeFactory atypeFactory) { @Override public Void visitExecutable(AnnotatedExecutableType t, Void p) { - List params = t.getParameterTypes(); - AnnotatedDeclaredType receiver = t.getReceiverType(); + AnnotatedDeclaredType receiverType = t.getReceiverType(); AnnotationMirror receiverAnno = - receiver == null ? null : receiver.getEffectiveAnnotationInHierarchy(TOP); + receiverType == null ? null : receiverType.getEffectiveAnnotationInHierarchy(TOP); boolean receiverHasManualAnno = receiverAnno != null && !AnnotationUtils.areSameByName(BOTTOM, receiverAnno); @@ -625,7 +624,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { && !AnnotationUtils.areSameByName(POLY, superReceiverAnno); if (!receiverHasManualAnno) { if (superReceiverHasManualAnno) { - receiver.replaceAnnotation(superReceiverAnno); + receiverType.replaceAnnotation(superReceiverAnno); } } @@ -641,13 +640,14 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { } } - List superParams = + List paramTypes = t.getParameterTypes(); + List superParamTypes = annotatedSuperMethod.getParameterTypes(); - if (params.size() == superParams.size()) { - for (int i = 0; i < superParams.size(); i++) { + if (paramTypes.size() == superParamTypes.size()) { + for (int i = 0; i < superParamTypes.size(); i++) { AnnotationMirror superParamAnno = - superParams.get(i).getPrimaryAnnotationInHierarchy(TOP); - AnnotationMirror paramAnno = params.get(i).getEffectiveAnnotationInHierarchy(TOP); + superParamTypes.get(i).getPrimaryAnnotationInHierarchy(TOP); + AnnotationMirror paramAnno = paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP); boolean paramHasManualAnno = paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); boolean superParamHasManualAnno = @@ -656,7 +656,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { && !AnnotationUtils.areSameByName(POLY, superParamAnno); if (!paramHasManualAnno) { if (superParamHasManualAnno) { - params.get(i).replaceAnnotation(superParamAnno); + paramTypes.get(i).replaceAnnotation(superParamAnno); } } } diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java index 5bada35c2633..0f41498938a7 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationStore.java @@ -7,7 +7,7 @@ public class AccumulationStore extends CFAbstractStore { /** - * Constructor matching super. + * Creates an AccumulationStore. * * @param analysis the analysis * @param sequentialSemantics if true, use sequential semantics; if false, use concurrent @@ -20,7 +20,7 @@ protected AccumulationStore( } /** - * Constructor matching super's copy constructor. + * Creates an AccumulationStore as a copy of the given store. * * @param other another store */ diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java index 9d2e6131c86d..7c1fa948c0e6 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java @@ -13,7 +13,7 @@ public class AccumulationVisitor extends BaseTypeVisitor { /** - * Constructor matching super. + * Creates an AccumulationVisitor. * * @param checker the checker */ From b34355335834a4226a7607a4d89916922b7f659d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:07:39 -0700 Subject: [PATCH 259/374] Documentation for `defaultCreateAbstractValue` --- .../checkerframework/framework/flow/CFAbstractAnalysis.java | 6 ++++++ .../framework/type/AnnotatedTypeFactory.java | 5 +++++ framework/tests/all-systems/InferTypeArgs2.java | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java index 502aea5533f1..217465a5c8e9 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java @@ -216,6 +216,7 @@ public T createTransferFunction() { public abstract @Nullable V createAbstractValue( AnnotationMirrorSet annotations, TypeMirror underlyingType); + // This cannot be inlined into `createAbstractValue()`, because the Java type system forbids it. /** Default implementation for {@link #createAbstractValue(AnnotationMirrorSet, TypeMirror)}. */ public @Nullable CFValue defaultCreateAbstractValue( CFAbstractAnalysis analysis, @@ -227,6 +228,11 @@ public T createTransferFunction() { return new CFValue(analysis, annotations, underlyingType); } + /** + * Returns the type hierarchy. + * + * @return the type hierarchy + */ public TypeHierarchy getTypeHierarchy() { return typeHierarchy; } 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 f61590b8458e..dd0dc7cf8fe2 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -1084,6 +1084,11 @@ protected TypeHierarchy createTypeHierarchy() { checker.hasOption("invariantArrays")); } + /** + * Returns the type hierarchy. + * + * @return the type hierarchy + */ public final TypeHierarchy getTypeHierarchy() { return typeHierarchy; } diff --git a/framework/tests/all-systems/InferTypeArgs2.java b/framework/tests/all-systems/InferTypeArgs2.java index f7e6fc03bd92..15bc2a010257 100644 --- a/framework/tests/all-systems/InferTypeArgs2.java +++ b/framework/tests/all-systems/InferTypeArgs2.java @@ -18,7 +18,7 @@ class InferTypeArgsAnalysis< V extends InferTypeArgs2, S extends CFAbstractStore, T extends CFAbstractTransfer> { - public CFValue defaultCreateAbstractValue(InferTypeArgsAnalysis analysis) { + public CFValue getCfValue(InferTypeArgsAnalysis analysis) { return new CFValue(analysis); } } From 20aa7d7b6d85b21b11bc930e8352e78c9beb5c80 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:08:24 -0700 Subject: [PATCH 260/374] Checkpoint --- ...llectionOwnershipAnnotatedTypeFactory.java | 74 +++++++------- .../CollectionOwnershipStore.java | 9 +- .../CollectionOwnershipTransfer.java | 96 +++++++++---------- .../CollectionOwnershipVisitor.java | 22 ++--- .../RLCCalledMethodsTransfer.java | 11 +-- .../common/basetype/BaseTypeVisitor.java | 15 ++- .../framework/flow/CFAbstractAnalysis.java | 2 +- .../framework/flow/CFAbstractValue.java | 29 +++--- 8 files changed, 121 insertions(+), 137 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index a5dc25184d16..6fce84f709a3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -101,13 +101,13 @@ public class CollectionOwnershipAnnotatedTypeFactory * hierarchy. */ public enum CollectionOwnershipType { - /** the @NotOwningCollection type */ + /** The @NotOwningCollection type. */ NotOwningCollection, - /** the @OwningCollection type */ + /** The @OwningCollection type. */ OwningCollection, - /** the @OwningCollectionWithoutObligation type */ + /** The @OwningCollectionWithoutObligation type. */ OwningCollectionWithoutObligation, - /** the @OwningCollectionBottom type */ + /** The @OwningCollectionBottom type. */ OwningCollectionBottom }; @@ -616,25 +616,25 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { AnnotatedExecutableType annotatedSuperMethod = CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(superElt); - AnnotatedDeclaredType superReceiver = annotatedSuperMethod.getReceiverType(); - AnnotationMirror superReceiverAnno = superReceiver.getPrimaryAnnotationInHierarchy(TOP); - boolean superReceiverHasManualAnno = - superReceiverAnno != null - && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno) - && !AnnotationUtils.areSameByName(POLY, superReceiverAnno); if (!receiverHasManualAnno) { + AnnotatedDeclaredType superReceiver = annotatedSuperMethod.getReceiverType(); + AnnotationMirror superReceiverAnno = superReceiver.getPrimaryAnnotationInHierarchy(TOP); + boolean superReceiverHasManualAnno = + superReceiverAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno) + && !AnnotationUtils.areSameByName(POLY, superReceiverAnno); if (superReceiverHasManualAnno) { receiverType.replaceAnnotation(superReceiverAnno); } } - AnnotatedTypeMirror superReturnType = annotatedSuperMethod.getReturnType(); - AnnotationMirror superReturnAnno = superReturnType.getPrimaryAnnotationInHierarchy(TOP); - boolean superReturnHasManualAnno = - superReturnAnno != null - && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno) - && !AnnotationUtils.areSameByName(POLY, superReturnAnno); if (!returnHasManualAnno) { + AnnotatedTypeMirror superReturnType = annotatedSuperMethod.getReturnType(); + AnnotationMirror superReturnAnno = superReturnType.getPrimaryAnnotationInHierarchy(TOP); + boolean superReturnHasManualAnno = + superReturnAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno) + && !AnnotationUtils.areSameByName(POLY, superReturnAnno); if (superReturnHasManualAnno) { returnType.replaceAnnotation(superReturnAnno); } @@ -645,16 +645,16 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { annotatedSuperMethod.getParameterTypes(); if (paramTypes.size() == superParamTypes.size()) { for (int i = 0; i < superParamTypes.size(); i++) { - AnnotationMirror superParamAnno = - superParamTypes.get(i).getPrimaryAnnotationInHierarchy(TOP); AnnotationMirror paramAnno = paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP); boolean paramHasManualAnno = paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); - boolean superParamHasManualAnno = - superParamAnno != null - && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno) - && !AnnotationUtils.areSameByName(POLY, superParamAnno); if (!paramHasManualAnno) { + AnnotationMirror superParamAnno = + superParamTypes.get(i).getPrimaryAnnotationInHierarchy(TOP); + boolean superParamHasManualAnno = + superParamAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno) + && !AnnotationUtils.areSameByName(POLY, superParamAnno); if (superParamHasManualAnno) { paramTypes.get(i).replaceAnnotation(superParamAnno); } @@ -662,7 +662,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { } } } - } + } // end "if (overriddenMethods != null)" if (isResourceCollection(returnType.getUnderlyingType())) { AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); @@ -716,33 +716,25 @@ protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, b } } - /* - * Default resource collection fields to @OwningCollection and resource collection parameters to - * @NotOwningCollection (inside the method). - * - * Resource collections are either java.lang.Iterable's and java.util.Iterator's, whose component has - * non-empty @MustCall type, as defined by the predicate isResourceCollection(AnnotatedTypeMirror). - */ + // Default resource collection fields to @OwningCollection and resource collection parameters + // to @NotOwningCollection (inside the method). + // + // A resource collection is a `Iterable` or `Iterator`, whose component has non-empty + // @MustCall type, as defined by the predicate isResourceCollection(AnnotatedTypeMirror). @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { super.addComputedTypeAnnotations(elt, type); if (elt instanceof VariableElement) { - boolean isField = elt.getKind() == ElementKind.FIELD; - boolean isParam = elt.getKind() == ElementKind.PARAMETER; - boolean isResourceCollection = isResourceCollection(type.getUnderlyingType()); - - if (isResourceCollection) { - if (isField) { + if (isResourceCollection(type.getUnderlyingType())) { + if (elt.getKind() == ElementKind.FIELD) { AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { type.replaceAnnotation(OWNINGCOLLECTION); } - } else if (isParam) { - // propagate annotation computed for parameter declaration - // to the use site - Element enclosingElement = elt.getEnclosingElement(); - ExecutableElement method = (ExecutableElement) enclosingElement; + } else if (elt.getKind() == ElementKind.PARAMETER) { + // Propagate the parameter annotation to the use site. + ExecutableElement method = (ExecutableElement) elt.getEnclosingElement(); AnnotatedExecutableType annotatedMethod = CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(method); List params = method.getParameters(); diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index 09618e352cda..e3bc90e25390 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -6,13 +6,14 @@ import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; /** - * The CollectionOwnership Store behaves like CFAbstractStore but keeps @OwningCollection fields in - * the store. This is justified by the strict access rules of such fields. Keeping the field in the - * store is required for verifying the postcondition annotation {@code @CollectionFieldDestructor}. + * The CollectionOwnership Store behaves like CFAbstractStore but keeps {@code @}{@link + * OwningCollection} fields in the store. This is justified by the strict access rules of such + * fields. Keeping the field in the store is required for verifying the postcondition annotation + * {@code @}{@link CollectionFieldDestructor}. */ public class CollectionOwnershipStore extends CFAbstractStore { - /** the annotated type factory */ + /** The annotated type factory. */ private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; /** diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 82cb9df15250..1ce16d924fd8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -44,7 +44,7 @@ public class CollectionOwnershipTransfer /** The checker. */ private final CollectionOwnershipChecker checker; - /** The MustCall type factory to manage temp vars. */ + /** The MustCall type factory to manage temporary variables. */ private final MustCallAnnotatedTypeFactory mcAtf; /** @@ -56,8 +56,8 @@ public class CollectionOwnershipTransfer public CollectionOwnershipTransfer( CollectionOwnershipAnalysis analysis, CollectionOwnershipChecker checker) { super(analysis); - atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); - mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); + this.atypeFactory = (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory(); + this.mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); this.checker = checker; } @@ -66,29 +66,27 @@ public TransferResult visitAssignment( AssignmentNode node, TransferInput in) { TransferResult res = super.visitAssignment(node, in); - Node lhs = node.getTarget(); - lhs = getNodeOrTempVar(lhs); - JavaExpression lhsJx = JavaExpression.fromNode(lhs); + Node lhs = getNodeOrTempVar(node.getTarget()); + JavaExpression lhsJE = JavaExpression.fromNode(lhs); - Node rhs = node.getExpression(); - rhs = getNodeOrTempVar(rhs); - CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); - CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs, coStore); + Node rhs = getNodeOrTempVar(node.getExpression()); + CollectionOwnershipType rhsType = + atypeFactory.getCoType(rhs, atypeFactory.getStoreBefore(node)); - // ownership transfer from rhs into lhs usually. - // special case desugared assignments of a temporary array variable - // and rhs being owning resource collection field + // Ownership transfer from rhs into lhs usually. + // Special case: desugared assignments of a temporary array variable + // and rhs being owning resource collection field. if (rhsType != null) { switch (rhsType) { case OwningCollection: case OwningCollectionWithoutObligation: - JavaExpression rhsJx = JavaExpression.fromNode(rhs); + JavaExpression rhsJE = JavaExpression.fromNode(rhs); if (node.isDesugaredFromEnhancedArrayForLoop() || atypeFactory.isOwningCollectionField( TreeUtils.elementFromTree(node.getExpression().getTree()))) { - replaceInStores(res, lhsJx, atypeFactory.NOTOWNINGCOLLECTION); + replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); } else { - replaceInStores(res, rhsJx, atypeFactory.NOTOWNINGCOLLECTION); + replaceInStores(res, rhsJE, atypeFactory.NOTOWNINGCOLLECTION); } break; default: @@ -99,7 +97,7 @@ public TransferResult visitAssignment( // if (assignmentOfOwningCollectionArrayElement) { // ExpressionTree arrayExpression = ((ArrayAccessTree) lhs.getTree()).getExpression(); - // JavaExpression arrayJx = JavaExpression.fromTree(arrayExpression); + // JavaExpression arrayJE = JavaExpression.fromTree(arrayExpression); // boolean inAssigningLoop = // MustCallOnElementsAnnotatedTypeFactory.doesAssignmentCreateArrayObligation( @@ -109,17 +107,17 @@ public TransferResult visitAssignment( // // not the assignment node. So, only transform if not in an assigning loop. // if (!inAssigningLoop) { // store = - // transformWriteToOwningCollection(arrayJx, arrayExpression, node.getExpression(), + // transformWriteToOwningCollection(arrayJE, arrayExpression, node.getExpression(), // store); // } return res; } /** - * Checks if the given AST tree is the condition for a collection-obligation-fulfilling and - * whether this loop calls all methods in the MustCall type of the elements of some collection. In - * this case, refine the type of the collection to @OwningCollectionWithoutObligation in the else - * store of the incoming transfer result. + * May refine the type of the collection to @OwningCollectionWithoutObligation in the else store + * of the incoming transfer result. Does so if the given AST tree is the condition for a + * collection-obligation-fulfilling loop that calls all methods in the MustCall type of the + * elements of some collection. * * @param res the incoming transfer result * @param tree the AST tree that is possibly the loop condition for a @@ -132,15 +130,15 @@ private TransferResult transformPotentiallyFu CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); if (loop != null) { CollectionOwnershipStore elseStore = res.getElseStore(); - JavaExpression collectionJx = JavaExpression.fromTree(loop.collectionTree); + JavaExpression collectionJE = JavaExpression.fromTree(loop.collectionTree); CollectionOwnershipType collectionCoType = atypeFactory.getCoType(loop.collectionTree); if (collectionCoType == CollectionOwnershipType.OwningCollection) { List mustCallValuesOfElements = atypeFactory.getMustCallValuesOfResourceCollectionComponent(loop.collectionTree); if (loop.getMethods().containsAll(mustCallValuesOfElements)) { - elseStore.clearValue(collectionJx); - elseStore.insertValue(collectionJx, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); + elseStore.clearValue(collectionJE); + elseStore.insertValue(collectionJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); return new ConditionalTransferResult<>( res.getResultValue(), res.getThenStore(), elseStore); } @@ -168,7 +166,7 @@ public TransferResult visitMethodInvocation( res = transferOwnershipForMethodInvocation(method, node, args, res); res = transformPotentiallyFulfillingLoop(res, node.getTree()); - // check whether the method is annotated @CreatesCollectionObligation + // Check whether the method is annotated @CreatesCollectionObligation. ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); boolean hasCreatesCollectionObligation = atypeFactory.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; @@ -176,11 +174,10 @@ public TransferResult visitMethodInvocation( atypeFactory.getDeclAnnotation(methodElement, CollectionFieldDestructor.class) != null; if (hasCreatesCollectionObligation) { Node receiverNode = node.getTarget().getReceiver(); - JavaExpression receiverJx = JavaExpression.fromNode(receiverNode); - CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); - if (atypeFactory.getCoType(receiverNode, coStore) + JavaExpression receiverJE = JavaExpression.fromNode(receiverNode); + if (atypeFactory.getCoType(receiverNode, atypeFactory.getStoreBefore(node)) == CollectionOwnershipType.OwningCollectionWithoutObligation) { - replaceInStores(res, receiverJx, atypeFactory.OWNINGCOLLECTION); + replaceInStores(res, receiverJE, atypeFactory.OWNINGCOLLECTION); } } if (hasCollectionFieldDestructor) { @@ -216,11 +213,10 @@ private TransferResult transferOwnershipForMe for (int i = 0; i < Math.min(args.size(), params.size()); i++) { VariableElement param = params.get(i); - Node arg = args.get(i); - arg = getNodeOrTempVar(arg); - JavaExpression argJx = JavaExpression.fromNode(arg); - CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); - CollectionOwnershipType argType = atypeFactory.getCoType(arg, coStore); + Node arg = getNodeOrTempVar(args.get(i)); + JavaExpression argJE = JavaExpression.fromNode(arg); + CollectionOwnershipType argType = + atypeFactory.getCoType(arg, atypeFactory.getStoreBefore(node)); CollectionOwnershipType paramType = atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); if (paramType == null) { @@ -254,7 +250,7 @@ private TransferResult transferOwnershipForMe checker.reportError( arg.getTree(), "transfer.owningcollection.field.ownership", arg.getTree().toString()); } else { - replaceInStores(res, argJx, atypeFactory.NOTOWNINGCOLLECTION); + replaceInStores(res, argJE, atypeFactory.NOTOWNINGCOLLECTION); } } } @@ -272,9 +268,9 @@ public TransferResult visitObjectCreation( List args = node.getArguments(); result = transferOwnershipForMethodInvocation(constructor, node, args, result); - // the return value defaulting logic cannot recognize that a diamond constructed collection + // The return value defaulting logic cannot recognize that a diamond-constructed collection // is a resource collection, as it runs before the type variable is inferred: - // List = new ArrayList<>(); + // List = new ArrayList<>(); // Thus, the following checks object creation expressions again on whether they are // resource collections with no type variables, and if they are @Bottom, they are // unrefined to @OwningCollection. Change the type of both the type var and the computed @@ -282,15 +278,15 @@ public TransferResult visitObjectCreation( CollectionOwnershipStore store = result.getRegularStore(); CFValue resultValue = result.getResultValue(); Node tempVarNode = getNodeOrTempVar(node); - JavaExpression tempVarJx = JavaExpression.fromNode(tempVarNode); + JavaExpression tempVarJE = JavaExpression.fromNode(tempVarNode); - CollectionOwnershipStore coStore = atypeFactory.getStoreBefore(node); - CollectionOwnershipType resolvedType = atypeFactory.getCoType(tempVarNode, coStore); + CollectionOwnershipType resolvedType = + atypeFactory.getCoType(tempVarNode, atypeFactory.getStoreBefore(node)); if (atypeFactory.isResourceCollection(node.getTree())) { boolean isDiamond = node.getTree().getTypeArguments().size() == 0; if (isDiamond && resolvedType == CollectionOwnershipType.OwningCollectionBottom) { - store.clearValue(tempVarJx); - store.insertValue(tempVarJx, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); + store.clearValue(tempVarJE); + store.insertValue(tempVarJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); resultValue = analysis.createSingleAnnotationValue( atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION, node.getType()); @@ -301,8 +297,7 @@ public TransferResult visitObjectCreation( } /** - * This method either creates or looks up the temp var t for node, and then updates the store to - * give t the same type as {@code node}. + * Updates the store to give the temp var for {@code node} the same type as {@code node}. * * @param node the node to be assigned to a temporary variable * @param result the transfer result containing the store to be modified @@ -324,7 +319,8 @@ public void updateStoreWithTempVar( } /** - * Inserts newAnno as the value into all stores (conditional or not) in the result for node. + * Inserts {@code newAnno} as the value into all stores (conditional or not) in the result for + * node. * * @param result the TransferResult holding the stores to modify * @param target the receiver whose value should be modified @@ -343,8 +339,8 @@ protected static void insertInStores( } /** - * Inserts newAnno as the value into all stores (conditional or not) in the result for node, - * clearing the previous value first. + * Inserts {@code newAnno} as the value into all stores (conditional or not) in the result for + * node, clearing the previous value first. * * @param result the TransferResult holding the stores to modify * @param target the receiver whose value should be modified @@ -366,8 +362,8 @@ protected static void replaceInStores( } /** - * Removes casts from {@code node} and returns the temp-var corresponding to it if it exists or - * else {@code node} with removed casts. + * Returns the temp-var corresponding to {@code node} if it exists, or else returns {@code node} + * with casts removed. * * @param node the node * @return the temp-var corresponding to {@code node} with casts removed if it exists or else diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 1c830305cfc7..b5e0a45c0cf8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -48,14 +48,14 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { } /** - * This method typically issues a warning if the result type of the constructor is not top, + * This method checks that the result type of a constructor is a supertype of the declared type on + * the class, if one exists. + * + *

This method typically issues a warning if the result type of the constructor is not top, * because in top-default type systems that indicates a potential problem. The Must Call Checker * does not need this warning, because it expects the type of all constructors to be {@code * OwningCollectionBottom} (by default). * - *

Instead, this method checks that the result type of a constructor is a supertype of the - * declared type on the class, if one exists. - * * @param constructorType an AnnotatedExecutableType for the constructor * @param constructorElement element that declares the constructor */ @@ -101,11 +101,11 @@ public Void visitVariable(VariableTree tree, Void p) { } /** - * Checks validity of an {@code OwningCollection} field {@code field}. Say the type of the - * elements of {@code field} is {@code @MustCall("m"}}. This method checks that the enclosing - * class of {@code field} has a type {@code @MustCall("m2")} for some method {@code m2}, and that - * {@code m2} has an annotation {@code @CollectionFieldDestructor("field")}, guaranteeing that the - * {@code @MustCall} obligation of the field will be satisfied. + * Checks validity of an {@code OwningCollection} field {@code field}. Say the element type {@code + * field} is {@code @MustCall("m"}}. This method checks that the enclosing class of {@code field} + * has a type {@code @MustCall("m2")} for some method {@code m2}, and that {@code m2} has an + * annotation {@code @CollectionFieldDestructor("field")}, guaranteeing that the {@code @MustCall} + * obligation of the field will be satisfied. * * @param fieldTree the declaration of the field to check */ @@ -118,17 +118,17 @@ private void checkOwningCollectionField(VariableTree fieldTree) { return; } - String error; RLCCalledMethodsAnnotatedTypeFactory rlAtf = ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(atypeFactory); Element enclosingElement = fieldElement.getEnclosingElement(); List enclosingMustCallValues = rlAtf.getMustCallValues(enclosingElement); + String error; if (enclosingMustCallValues == null) { error = " The enclosing element " + ElementUtils.getQualifiedName(enclosingElement) - + " doesn't have a @MustCall annotation"; + + " has no @MustCall annotation"; } else if (enclosingMustCallValues.isEmpty()) { error = " The enclosing element " diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 4cd96fb55595..c50742e0041b 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -71,9 +71,9 @@ public AccumulationStore initialStore( (RLCCalledMethodsAnnotatedTypeFactory) this.analysis.getTypeFactory(); for (RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop loop : RLCCalledMethodsAnnotatedTypeFactory.getPotentiallyFulfillingLoops()) { - IteratedCollectionElement collectionElementJx = + IteratedCollectionElement collectionElementJE = new IteratedCollectionElement(loop.collectionElementNode, loop.collectionElementTree); - store.insertValue(collectionElementJx, cmAtf.top); + store.insertValue(collectionElementJE, cmAtf.top); } return store; } @@ -250,12 +250,7 @@ public void updateStoreWithTempVar( if (anm == null) { anm = rlTypeFactory.top; } - if (result.containsTwoStores()) { - result.getThenStore().insertValue(localExp, anm); - result.getElseStore().insertValue(localExp, anm); - } else { - result.getRegularStore().insertValue(localExp, anm); - } + insertIntoStores(result, localExp, anm); } } } diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 86b17966947a..4a2843b87da7 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -3018,13 +3018,12 @@ protected void checkExceptionParameter(CatchTree tree) { } /** - * Returns a set of AnnotationMirrors that is a lower bound for exception parameters. + * Returns a set of AnnotationMirrors that is a lower bound for exception parameters. If a user + * writes a more specific (lower) annotation that this on an exception parameter, the Checker + * Framework issues a warning saying that the written annotation cannot be verified. * *

This implementation returns top; subclasses can change this behavior. * - *

Note: by default this method is called by {@link #getThrowUpperBoundAnnotations()}, so that - * this annotation is enforced. - * * @return set of annotation mirrors, one per hierarchy, that form a lower bound of annotations * that can be written on an exception parameter */ @@ -3083,12 +3082,12 @@ protected void checkThrownExpression(ThrowTree tree) { } /** - * Returns a set of AnnotationMirrors that is a upper bound for thrown exceptions. + * Returns a set of AnnotationMirrors that is an upper bound for thrown exceptions. * - *

Note: by default this method is returns by getExceptionParameterLowerBoundAnnotations(), so - * that this annotation is enforced. + *

Note: by default this method returns {@code getExceptionParameterLowerBoundAnnotations()}, + * so that this annotation is enforced. * - *

(Default is top) + *

(Default is top.) * * @return set of annotation mirrors, one per hierarchy, that form an upper bound of thrown * expressions diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java index 502aea5533f1..39effeab1f33 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java @@ -217,7 +217,7 @@ public T createTransferFunction() { AnnotationMirrorSet annotations, TypeMirror underlyingType); /** Default implementation for {@link #createAbstractValue(AnnotationMirrorSet, TypeMirror)}. */ - public @Nullable CFValue defaultCreateAbstractValue( + public final @Nullable CFValue defaultCreateAbstractValue( CFAbstractAnalysis analysis, AnnotationMirrorSet annotations, TypeMirror underlyingType) { diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java index d471e9f64321..01a7eec34b0e 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java @@ -51,10 +51,10 @@ * * @param the values that this CFAbstractValue wraps */ -public abstract class CFAbstractValue> implements AbstractValue { +public abstract class CFAbstractValue implements AbstractValue { /** The analysis class this value belongs to. */ - protected final CFAbstractAnalysis analysis; + protected final CFAbstractAnalysis analysis; /** The type factory. */ protected final AnnotatedTypeFactory atypeFactory; @@ -77,7 +77,7 @@ public abstract class CFAbstractValue> implements A */ @SuppressWarnings("this-escape") protected CFAbstractValue( - CFAbstractAnalysis analysis, + CFAbstractAnalysis analysis, AnnotationMirrorSet annotations, TypeMirror underlyingType) { this.analysis = analysis; @@ -245,10 +245,10 @@ public String toString() { * @param backup the value to use if {@code this} and {@code other} are incomparable * @return the more specific of two values {@code this} and {@code other} */ - public V mostSpecific(@Nullable V other, @Nullable V backup) { + public CFValue mostSpecific(@Nullable CFValue other, @Nullable CFValue backup) { if (other == null) { @SuppressWarnings("unchecked") - V v = (V) this; + CFValue v = (CFValue) this; return v; } Types types = analysis.getTypes(); @@ -294,7 +294,7 @@ private class MostSpecificVisitor extends AnnotationSetCombiner { * * @param backup value to use if no most specific value is found */ - public MostSpecificVisitor(V backup) { + public MostSpecificVisitor(CFValue backup) { if (backup != null) { this.backupAMSet = backup.getAnnotations(); // this.backupTypeMirror = backup.getUnderlyingType(); @@ -437,7 +437,7 @@ private final AnnotationMirror lowestQualifier(AnnotationMirror qual1, Annotatio * of this method. */ @Override - public final V leastUpperBound(@Nullable V other) { + public final CFValue leastUpperBound(@Nullable CFValue other) { return upperBound(other, false); } @@ -454,7 +454,7 @@ public final V leastUpperBound(@Nullable V other) { * least upper bound * @return the least upper bound of two abstract values */ - public final V leastUpperBound(@Nullable V other, TypeMirror typeMirror) { + public final CFValue leastUpperBound(@Nullable CFValue other, TypeMirror typeMirror) { return upperBound(other, typeMirror, false); } @@ -483,7 +483,7 @@ public final V leastUpperBound(@Nullable V other, TypeMirror typeMirror) { * @param previous must be the previous value * @return an upper bound of two values that is wider than the least upper bound of the two values */ - public final V widenUpperBound(@Nullable V previous) { + public final CFValue widenUpperBound(@Nullable CFValue previous) { return upperBound(previous, true); } @@ -494,10 +494,10 @@ public final V widenUpperBound(@Nullable V previous) { * @param shouldWiden true if the lub should perform widening * @return the least upper bound of this and {@code other} */ - private V upperBound(@Nullable V other, boolean shouldWiden) { + private CFValue upperBound(@Nullable CFValue other, boolean shouldWiden) { if (other == null) { @SuppressWarnings("unchecked") - V v = (V) this; + CFValue v = (CFValue) this; return v; } ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); @@ -521,7 +521,8 @@ private V upperBound(@Nullable V other, boolean shouldWiden) { * @param shouldWiden true if the method should perform widening * @return an upper bound of this and {@code other} */ - protected V upperBound(@Nullable V other, TypeMirror upperBoundTypeMirror, boolean shouldWiden) { + protected CFValue upperBound( + @Nullable CFValue other, TypeMirror upperBoundTypeMirror, boolean shouldWiden) { ValueLub valueLub = new ValueLub(shouldWiden); AnnotationMirrorSet lub = valueLub.combineSets( @@ -638,10 +639,10 @@ public ValueLub(boolean shouldWiden) { * @param other another value * @return the greatest lower bound of two values */ - public V greatestLowerBound(@Nullable V other) { + public CFValue greatestLowerBound(@Nullable CFValue other) { if (other == null) { @SuppressWarnings("unchecked") - V v = (V) this; + CFValue v = (CFValue) this; return v; } ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); From 94c273040a5dbfe2f817f18c383c705a749c9024 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:15:34 -0700 Subject: [PATCH 261/374] Add curly braces around `then` clauses --- .../afu/scenelib/el/ADeclaration.java | 8 ++++++-- .../afu/scenelib/io/IndexFileParser.java | 4 +++- .../io/classfile/ClassAnnotationSceneWriter.java | 8 ++++++-- .../EnsuresCalledMethodOnExceptionContract.java | 8 ++++++-- .../RLCCalledMethodsVisitor.java | 8 ++++++-- .../common/value/util/Range.java | 8 ++++++-- .../javacutil/AnnotationMirrorMap.java | 16 ++++++++++++---- 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java index 4cd416ecb6a4..b562ac0bb93c 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/el/ADeclaration.java @@ -84,8 +84,12 @@ public boolean isEmpty() { @Override public void prune() { super.prune(); - if (insertAnnotations != null) insertAnnotations.prune(); - if (insertTypecasts != null) insertTypecasts.prune(); + if (insertAnnotations != null) { + insertAnnotations.prune(); + } + if (insertTypecasts != null) { + insertTypecasts.prune(); + } } @Override diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/IndexFileParser.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/IndexFileParser.java index b2ed9b09d95f..926c5ee4ead2 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/IndexFileParser.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/IndexFileParser.java @@ -256,7 +256,9 @@ private void expectKeyword(String s) throws IOException, ParseException { private boolean isValidIdentifier(String x) { if (x.length() == 0 || !Character.isJavaIdentifierStart(x.charAt(0)) - || knownKeywords.contains(x)) return false; + || knownKeywords.contains(x)) { + return false; + } for (int i = 1; i < x.length(); i++) { if (!Character.isJavaIdentifierPart(x.charAt(i))) { return false; diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java index ed4e7aeaf11a..48d25fca072c 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java @@ -435,7 +435,9 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { // If annotation exists in scene, and in overwrite mode, // return empty visitor, annotation from scene will be visited later. - if (aField.lookup(classDescToName(descriptor)) != null && overwrite) return null; + if (aField.lookup(classDescToName(descriptor)) != null && overwrite) { + return null; + } return fv.visitAnnotation(descriptor, visible); } @@ -448,7 +450,9 @@ public AnnotationVisitor visitTypeAnnotation( // If annotation exists in scene, and in overwrite mode, // return empty visitor, annotation from scene will be visited later. - if (aField.lookup(classDescToName(descriptor)) != null && overwrite) return null; + if (aField.lookup(classDescToName(descriptor)) != null && overwrite) { + return null; + } return fv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); } diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java index 0bddc54f3a62..49123de65bde 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java @@ -52,8 +52,12 @@ public String getMethod() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof EnsuresCalledMethodOnExceptionContract)) return false; + if (this == o) { + return true; + } + if (!(o instanceof EnsuresCalledMethodOnExceptionContract)) { + return false; + } EnsuresCalledMethodOnExceptionContract that = (EnsuresCalledMethodOnExceptionContract) o; return expression.equals(that.expression) && method.equals(that.method); } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java index 367f400d6207..c110ea826e88 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsVisitor.java @@ -443,8 +443,12 @@ public DestructorObligation( @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } DestructorObligation that = (DestructorObligation) o; return mustCallMethod.equals(that.mustCallMethod) && exitKind == that.exitKind; } diff --git a/framework/src/main/java/org/checkerframework/common/value/util/Range.java b/framework/src/main/java/org/checkerframework/common/value/util/Range.java index f5ec647f17c8..7e0dabbd0eaf 100644 --- a/framework/src/main/java/org/checkerframework/common/value/util/Range.java +++ b/framework/src/main/java/org/checkerframework/common/value/util/Range.java @@ -133,8 +133,12 @@ public static Range create(Collection values) { long max = min; for (Number value : values) { long current = value.longValue(); - if (min > current) min = current; - if (max < current) max = current; + if (min > current) { + min = current; + } + if (max < current) { + max = current; + } } return create(min, max); } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java index 89da1bdafb2c..33a449068985 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java @@ -187,7 +187,9 @@ public boolean equals(@Nullable Object o) { if (o == this) { return true; } - if (!(o instanceof AnnotationMirrorMap)) return false; + if (!(o instanceof AnnotationMirrorMap)) { + return false; + } AnnotationMirrorMap m = (AnnotationMirrorMap) o; if (m.size() != size()) { return false; @@ -198,9 +200,13 @@ public boolean equals(@Nullable Object o) { AnnotationMirror key = e.getKey(); V value = e.getValue(); if (value == null) { - if (!(m.get(key) == null && m.containsKey(key))) return false; + if (!(m.get(key) == null && m.containsKey(key))) { + return false; + } } else { - if (!value.equals(m.get(key))) return false; + if (!value.equals(m.get(key))) { + return false; + } } } } catch (ClassCastException | NullPointerException unused) { @@ -213,7 +219,9 @@ public boolean equals(@Nullable Object o) { @Override public int hashCode() { int result = 0; - for (Entry entry : entrySet()) result += entry.hashCode(); + for (Entry entry : entrySet()) { + result += entry.hashCode(); + } return result; } } From 0e80e9d51d13842d38fcba3a64cc4dfcb53e633a Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:17:30 -0700 Subject: [PATCH 262/374] Add curly braces --- ...llectionOwnershipAnnotatedTypeFactory.java | 8 +++-- .../MustCallAnnotatedTypeFactory.java | 4 ++- .../checker/mustcall/MustCallVisitor.java | 24 +++++++++---- .../MustCallConsistencyAnalyzer.java | 8 +++-- .../resourceleak/ResourceLeakUtils.java | 36 ++++++++++++++----- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 6fce84f709a3..75bb5a742413 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -348,7 +348,9 @@ public boolean isOwningCollectionParameter(Element elt) { * @return true if the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { - if (tree == null) return false; + if (tree == null) { + return false; + } MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); AnnotatedTypeMirror treeMcType = null; try { @@ -497,7 +499,9 @@ public CollectionOwnershipType getCoType(Collection annos) { return null; } for (AnnotationMirror anm : annos) { - if (anm == null) continue; + if (anm == null) { + continue; + } if (AnnotationUtils.areSame(anm, NOTOWNINGCOLLECTION)) { return CollectionOwnershipType.NotOwningCollection; } else if (AnnotationUtils.areSame(anm, OWNINGCOLLECTION)) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 900958b6b3e2..755cac18f5b6 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -176,7 +176,9 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclaredType adt) { if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) { for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) { - if (typeArg == null) continue; + if (typeArg == null) { + continue; + } if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) { if (tree != null && tree instanceof NewClassTree) { if (((NewClassTree) tree).getTypeArguments().isEmpty()) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 0d2a0b49263b..2fce408a499c 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -424,7 +424,9 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { if (collectionEltNodes != null) { nodeForCollectionElt = collectionEltNodes.iterator().next(); } - if (loopUpdateBlock == null || loopConditionBlock == null) return; + if (loopUpdateBlock == null || loopConditionBlock == null) { + return; + } // add the blocks into a static datastructure in the calledmethodsatf, such that it can // analyze // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees @@ -483,7 +485,9 @@ protected Name verifyAllElementsAreCalledOn( return null; } // verify that condition is of the form: i < something - if (!(condition.getLeftOperand() instanceof IdentifierTree)) return null; + if (!(condition.getLeftOperand() instanceof IdentifierTree)) { + return null; + } if (initVar.getName() != ((IdentifierTree) condition.getLeftOperand()).getName() // i=0 and i elementRawType = TypesUtils.getClassFromType(type); - if (elementRawType == null) return false; + if (elementRawType == null) { + return false; + } return Iterable.class.isAssignableFrom(elementRawType) || Iterator.class.isAssignableFrom(elementRawType) || Map.class.isAssignableFrom(elementRawType); @@ -303,9 +311,13 @@ public static boolean isCollection(TypeMirror type) { * @return true if type is a java.util.Iterator */ public static boolean isIterator(TypeMirror type) { - if (type == null) return false; + if (type == null) { + return false; + } Class elementRawType = TypesUtils.getClassFromType(type); - if (elementRawType == null) return false; + if (elementRawType == null) { + return false; + } return Iterator.class.isAssignableFrom(elementRawType); } @@ -419,7 +431,9 @@ public static List getMcValues( * @return true if the passed {@code TypeMirror} has a manual {@code MustCallUnknown} annotation */ public static boolean hasManualMustCallUnknownAnno(TypeMirror typeMirror) { - if (typeMirror == null) return false; + if (typeMirror == null) { + return false; + } for (AnnotationMirror paramAnno : typeMirror.getAnnotationMirrors()) { if (AnnotationUtils.areSameByName(paramAnno, MustCallUnknown.class.getCanonicalName())) { return true; @@ -439,9 +453,13 @@ public static boolean hasManualMustCallUnknownAnno(TypeMirror typeMirror) { */ public static boolean hasManualMustCallUnknownAnno( AnnotatedTypeMirror annotatedTypeMirror, MustCallAnnotatedTypeFactory mcAtf) { - if (annotatedTypeMirror == null) return false; + if (annotatedTypeMirror == null) { + return false; + } AnnotationMirror manualMcAnno = annotatedTypeMirror.getPrimaryAnnotationInHierarchy(mcAtf.TOP); - if (manualMcAnno == null) return false; + if (manualMcAnno == null) { + return false; + } if (AnnotationUtils.areSameByName(manualMcAnno, MustCallUnknown.class.getCanonicalName())) { return true; } From 41396e0729b202ab9849b2bde70df46eac48b0e6 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:51:45 -0700 Subject: [PATCH 263/374] Code review edits --- .../collectionownership/messages.properties | 2 +- .../MustCallAnnotatedTypeFactory.java | 11 ++- .../checker/mustcall/MustCallVisitor.java | 89 +++++++++---------- .../MustCallConsistencyAnalyzer.java | 2 +- .../checker/resourceleak/messages.properties | 4 +- 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 0b32f731986d..c2d226ef01c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,4 +1,4 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s -transfer.owningcollection.field.ownership=Method invocation transfers the ownership away from field %s, which is unsafe. An @OwningCollection field can never lose ownership. +transfer.owningcollection.field.ownership=Method invocation unsafely transfers the ownership away from field %s. An @OwningCollection field can never lose ownership. illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 755cac18f5b6..7d978cfbbb14 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -230,17 +230,16 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar * *

This is necessary, as the type variable upper bounds for collections is * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, - * the type parameter does default to {@code @MustCallUnknown}, which is both unsound and - * imprecise. + * the type parameter defaults to {@code @MustCallUnknown}, which is both unsound and imprecise. * *

This method changes the type parameter annotations for declared types directly. The other * overload with access to {@code Element}s handles type parameter annotations for method return * types and parameters, such that the changes are 'visible' at call-site as well as within the * method. Changing this on the {@code Tree} is not sufficient. The reason that declared types are - * handled here is that for object initializations where the type parameter is left for inference, - * we don't want to change the type parameter annotation here, but wait for the inference instead, - * which instantiates it with the inferred type and corresponding annotation. {@code new - * Object<>()} Access to the {@code Tree} allows us to detect whether we have a new class tree + * handled here is that for object initializations where the type parameter is left for inference + * (e.g., {@code new Object<>()}), we don't want to change the type parameter annotation here, but + * wait for the inference instead, which instantiates it with the inferred type and corresponding + * annotation. Access to the {@code Tree} allows us to detect whether we have a new class tree * without type parameters. */ @Override diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 2fce408a499c..dff014d11d51 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -352,17 +352,13 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return null; } - /* - * SECTION: syntactically match for-loops that iterate over all elements of a collection on - * the AST - */ + // /////////////////////////////////////////////////////////////////////////// + // syntactically match for-loops that iterate over all elements of a collection on the AST + // /** - * Checks through pattern-matching whether the loop calls a method on entries of an - * {@code @OwningCollection}. - * - *

If yes, this is marked in some static datastructures in the - * {@code @CollectionOwnershipAnnotatedTypeFactory} + * Records, in the {@code @CollectionOwnershipAnnotatedTypeFactory}, loops that call a method on + * entries of an {@code @OwningCollection}. */ @Override public Void visitForLoop(ForLoopTree tree, Void p) { @@ -376,7 +372,7 @@ public Void visitForLoop(ForLoopTree tree, Void p) { /** * Marks the for-loop if it potentially fulfills collection obligations of a collection. * - * @param tree forlooptree + * @param tree a `for` loop with exactly one loop variable */ private void patternMatchFulfillingLoop(ForLoopTree tree) { List loopBodyStatementList; @@ -387,7 +383,7 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { loopBodyStatementList = Collections.singletonList(tree.getStatement()); } StatementTree init = tree.getInitializer().get(0); - ExpressionTree condition = tree.getCondition(); + ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); ExpressionStatementTree update = tree.getUpdate().get(0); if (!(condition instanceof BinaryTree)) { return; @@ -400,26 +396,27 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { ExpressionTree collectionElementTree = preMatchLoop(loopBodyStatementList, identifierInHeader, iterator); if (collectionElementTree != null) { - // pattern match succeeded, now mark the loop in the respective datastructures - Set conditionNodes = atypeFactory.getNodesForTree(condition); - Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); - Set updateNodes = atypeFactory.getNodesForTree(update.getExpression()); + // Pattern match succeeded, now mark the loop in the respective datastructures. + Block loopConditionBlock = null; - for (Node node : conditionNodes) { + for (Node node : atypeFactory.getNodesForTree(condition)) { Block blockOfNode = node.getBlock(); if (blockOfNode != null) { loopConditionBlock = blockOfNode; break; } } + Block loopUpdateBlock = null; - for (Node node : updateNodes) { + for (Node node : atypeFactory.getNodesForTree(update.getExpression())) { Block blockOfNode = node.getBlock(); if (blockOfNode != null) { loopUpdateBlock = blockOfNode; break; } } + + Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); Node nodeForCollectionElt = null; if (collectionEltNodes != null) { nodeForCollectionElt = collectionEltNodes.iterator().next(); @@ -427,13 +424,10 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { if (loopUpdateBlock == null || loopConditionBlock == null) { return; } - // add the blocks into a static datastructure in the calledmethodsatf, such that it can - // analyze - // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees - // to the static datastructure in McoeAtf) - assert (loopConditionBlock instanceof SingleSuccessorBlock); + // Add the blocks into a static datastructure in the calledmethodsatf, such that it can + // analyze them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds + // the trees to the static datastructure in McoeAtf). Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); - assert (conditionalBlock instanceof ConditionalBlock); Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); RLCCalledMethodsAnnotatedTypeFactory.addPotentiallyFulfillingLoop( collectionTreeFromExpression(collectionElementTree), @@ -447,53 +441,59 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { } /** - * Decides for a for-loop header whether the loop iterates over all elements of some collection - * based on a pattern-match with one-sided error with the following rules: + * Conservatively decides whether a loop iterates over all elements of some collection, using the + * following rules: * *

    *
  • only one loop variable *
  • initialization must be of the form i = 0 *
  • condition must be of the form (i < col.size()) - *
  • update must be prefix or postfix + *
  • update must be prefix or postfix {@code ++} *
* * Returns: * *
    - *
  • null if any rule is violated + *
  • null, if any of the above rules is violated *
  • the name of the collection if the loop condition is of the form (i < col.size()) *
* * @param init the initializer of the loop * @param condition the loop condition * @param update the loop update - * @return null if any rule is violated, or the name of the collection if the loop condition is of - * the form (i < col.size()) + * @return the name of the collection that the loop iterates over all elements of, or null */ protected Name verifyAllElementsAreCalledOn( StatementTree init, BinaryTree condition, ExpressionStatementTree update) { Tree.Kind updateKind = update.getExpression().getKind(); if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { UnaryTree inc = (UnaryTree) update.getExpression(); - // verify update is of form i++ or ++i and init is variable initializer + + // Verify update is of form i++ or ++i and init is variable initializer. if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) return null; VariableTree initVar = (VariableTree) init; - // verify that intializer is i=0 + + // Verify that intializer is i=0. if (!(initVar.getInitializer() instanceof LiteralTree) || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { return null; } - // verify that condition is of the form: i < something + + // Verify that condition is of the form: i < something. if (!(condition.getLeftOperand() instanceof IdentifierTree)) { return null; } - if (initVar.getName() - != ((IdentifierTree) condition.getLeftOperand()).getName() // i=0 and iThat is, the names are equal. + * header. That is, returns true if the names are equal. * *

Returns false if any argument is null. * diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 36a9daceca1b..a5a07ced38c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -282,7 +282,7 @@ public Obligation(Set resourceAliases, Set whenTo *

We need this method since we frequently need to replace obligations. If the old obligation * was of a certain subclass, we want the replacement to be as well. Dynamic dispatch then * allows us to simply call getReplacement() on an obligation and get the replacement of the - * right (sub)class. + * correct (sub)class. * * @param resourceAliases set of resource aliases for the new obligation * @param whenToEnforce when this Obligation should be enforced diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index 8ac9bae77cf7..2e3209b40c6c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -9,6 +9,6 @@ mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope w mustcallalias.method.return.and.param=@MustCallAlias annotations must appear in pairs (one on a return type and one on a parameter type).%nBut %s required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).%nThe type of object is: %s.%nReason for going out of scope: %s unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s -transfer.owningcollection.field.ownership=%s transfers the ownership away from field %s, which is unsafe. Consider annotating the return type @NotOwningCollection. +transfer.owningcollection.field.ownership=%s unsafely transfers the ownership away from field %s. Consider annotating the return type @NotOwningCollection. foreign.owningcollection.field.access=Illegal access of field %s. Cannot access a resource collection field not owned by this class instance. -illegal.owningcollection.field.assignment=The right-hand side of a field initializer assignment to an owning resource collection field must be of type @OwningCollectionWithoutObligation, but is of type %s. Consider moving the assignment into the constructor. +illegal.owningcollection.field.assignment=Owning resource-collection field initializer assigned to type %s. Expected @OwningCollectionWithoutObligation. Consider moving the assignment into the constructor. From a00e8830c017ae4ef96b96c936242463e91c512e Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 15:58:52 -0700 Subject: [PATCH 264/374] Code review edits --- .../MustCallConsistencyAnalyzer.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index a5a07ced38c0..af1cbe97de52 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -294,16 +294,15 @@ public Obligation getReplacement( } /** - * Creates and returns an obligation derived from the given tree that is either an {@code - * ExpressionTree} or a {@code VariableTree}. + * Creates and returns an obligation derived from the given expression. * - * @param tree the tree from which the Obligation is to be created. Must be ExpressionTree or - * VariableTree. + * @param tree the tree from which the Obligation is to be created. Must be {@code + * ExpressionTree} or {@code VariableTree}. * @return an obligation derived from the given tree */ public static Obligation fromTree(Tree tree) { - JavaExpression jx = null; - Element elem = null; + JavaExpression jx; + Element elem; if (tree instanceof ExpressionTree) { jx = JavaExpression.fromTree((ExpressionTree) tree); elem = TreeUtils.elementFromTree((ExpressionTree) tree); @@ -508,7 +507,7 @@ public int hashCode() { } } - /** Obligation for calling a certain method on all elements of a collection. */ + /** Obligation to call a certain method on all elements of a collection. */ static class CollectionObligation extends Obligation { /** The method that must be called on all elements of the collection. */ @@ -541,11 +540,10 @@ private CollectionObligation(Obligation obligation, String mustCallMethod) { } /** - * Creates and returns a CollectionObligation derived from the given tree that is either an - * {@code ExpressionTree} or a {@code VariableTree}. + * Creates and returns a CollectionObligation derived from the given expression. * - * @param tree the tree from which the CollectionObligation is to be created. Must be - * ExpressionTree or VariableTree. + * @param tree the tree from which to create the CollectionObligation. Must be {@code + * ExpressionTree} or {@code VariableTree}. * @param mustCallMethod the method that must be called on the elements of the collection * @return a CollectionObligation derived from the given tree */ @@ -572,12 +570,14 @@ public CollectionObligation getReplacement( @Override public boolean equals(@Nullable Object obj) { - if (!super.equals(obj)) { + if (obj == this) { + return true; + } + if (!(obj instanceof CollectionObligation)) { return false; - } else { - return (obj instanceof CollectionObligation) - && ((CollectionObligation) obj).mustCallMethod.equals(this.mustCallMethod); } + return super.equals(obj) + && ((CollectionObligation) obj).mustCallMethod.equals(this.mustCallMethod); } } @@ -762,10 +762,10 @@ public void analyze(ControlFlowGraph cfg) { } /** - * Adds {@code CollectionObligation}s if the return type is {@code @OwningCollection}. + * Adds {@code CollectionObligation}s if the type of {@code node} is {@code @OwningCollection}. * * @param obligations the set of tracked obligations - * @param node the node of the return expression + * @param node the node of a return expression */ private void addObligationsForOwningCollectionReturn(Set obligations, Node node) { LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node); From 2e8198223529a9e26c5cdd5868d0f0b20360b96b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 16:00:50 -0700 Subject: [PATCH 265/374] Revert experimental change --- .../framework/flow/CFAbstractValue.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java index 01a7eec34b0e..d471e9f64321 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java @@ -51,10 +51,10 @@ * * @param the values that this CFAbstractValue wraps */ -public abstract class CFAbstractValue implements AbstractValue { +public abstract class CFAbstractValue> implements AbstractValue { /** The analysis class this value belongs to. */ - protected final CFAbstractAnalysis analysis; + protected final CFAbstractAnalysis analysis; /** The type factory. */ protected final AnnotatedTypeFactory atypeFactory; @@ -77,7 +77,7 @@ public abstract class CFAbstractValue implements AbstractValue */ @SuppressWarnings("this-escape") protected CFAbstractValue( - CFAbstractAnalysis analysis, + CFAbstractAnalysis analysis, AnnotationMirrorSet annotations, TypeMirror underlyingType) { this.analysis = analysis; @@ -245,10 +245,10 @@ public String toString() { * @param backup the value to use if {@code this} and {@code other} are incomparable * @return the more specific of two values {@code this} and {@code other} */ - public CFValue mostSpecific(@Nullable CFValue other, @Nullable CFValue backup) { + public V mostSpecific(@Nullable V other, @Nullable V backup) { if (other == null) { @SuppressWarnings("unchecked") - CFValue v = (CFValue) this; + V v = (V) this; return v; } Types types = analysis.getTypes(); @@ -294,7 +294,7 @@ private class MostSpecificVisitor extends AnnotationSetCombiner { * * @param backup value to use if no most specific value is found */ - public MostSpecificVisitor(CFValue backup) { + public MostSpecificVisitor(V backup) { if (backup != null) { this.backupAMSet = backup.getAnnotations(); // this.backupTypeMirror = backup.getUnderlyingType(); @@ -437,7 +437,7 @@ private final AnnotationMirror lowestQualifier(AnnotationMirror qual1, Annotatio * of this method. */ @Override - public final CFValue leastUpperBound(@Nullable CFValue other) { + public final V leastUpperBound(@Nullable V other) { return upperBound(other, false); } @@ -454,7 +454,7 @@ public final CFValue leastUpperBound(@Nullable CFValue other) { * least upper bound * @return the least upper bound of two abstract values */ - public final CFValue leastUpperBound(@Nullable CFValue other, TypeMirror typeMirror) { + public final V leastUpperBound(@Nullable V other, TypeMirror typeMirror) { return upperBound(other, typeMirror, false); } @@ -483,7 +483,7 @@ public final CFValue leastUpperBound(@Nullable CFValue other, TypeMirror typeMir * @param previous must be the previous value * @return an upper bound of two values that is wider than the least upper bound of the two values */ - public final CFValue widenUpperBound(@Nullable CFValue previous) { + public final V widenUpperBound(@Nullable V previous) { return upperBound(previous, true); } @@ -494,10 +494,10 @@ public final CFValue widenUpperBound(@Nullable CFValue previous) { * @param shouldWiden true if the lub should perform widening * @return the least upper bound of this and {@code other} */ - private CFValue upperBound(@Nullable CFValue other, boolean shouldWiden) { + private V upperBound(@Nullable V other, boolean shouldWiden) { if (other == null) { @SuppressWarnings("unchecked") - CFValue v = (CFValue) this; + V v = (V) this; return v; } ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); @@ -521,8 +521,7 @@ private CFValue upperBound(@Nullable CFValue other, boolean shouldWiden) { * @param shouldWiden true if the method should perform widening * @return an upper bound of this and {@code other} */ - protected CFValue upperBound( - @Nullable CFValue other, TypeMirror upperBoundTypeMirror, boolean shouldWiden) { + protected V upperBound(@Nullable V other, TypeMirror upperBoundTypeMirror, boolean shouldWiden) { ValueLub valueLub = new ValueLub(shouldWiden); AnnotationMirrorSet lub = valueLub.combineSets( @@ -639,10 +638,10 @@ public ValueLub(boolean shouldWiden) { * @param other another value * @return the greatest lower bound of two values */ - public CFValue greatestLowerBound(@Nullable CFValue other) { + public V greatestLowerBound(@Nullable V other) { if (other == null) { @SuppressWarnings("unchecked") - CFValue v = (CFValue) this; + V v = (V) this; return v; } ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); From 37c76a51f9daa2d41951a00f48ded1ef865101e0 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 16:05:04 -0700 Subject: [PATCH 266/374] Make `insertIntoStores` non-static --- .../checkerframework/framework/flow/CFAbstractTransfer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java index 981ff59908ea..5560bb5cbf1f 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -1505,8 +1505,8 @@ public TransferResult visitExpressionStatement( * @param target the receiver whose value should be modified * @param newAnno the new value */ - protected static void insertIntoStores( - TransferResult result, JavaExpression target, AnnotationMirror newAnno) { + protected void insertIntoStores( + TransferResult result, JavaExpression target, AnnotationMirror newAnno) { if (result.containsTwoStores()) { result.getThenStore().insertValue(target, newAnno); result.getElseStore().insertValue(target, newAnno); From 66d4ca8a512e5f2ee267a5053a2d8b0f1348d026 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 16:22:52 -0700 Subject: [PATCH 267/374] Documentation fixes --- .../collectionownership/CollectionOwnershipStore.java | 2 ++ .../framework/flow/CFAbstractAnalysis.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java index e3bc90e25390..80a495a93fb1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java @@ -1,5 +1,7 @@ package org.checkerframework.checker.collectionownership; +import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; +import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFValue; diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java index f6bd1fb0b5eb..15a1d08ec8e9 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractAnalysis.java @@ -217,7 +217,14 @@ public T createTransferFunction() { AnnotationMirrorSet annotations, TypeMirror underlyingType); // This cannot be inlined into `createAbstractValue()`, because the Java type system forbids it. - /** Default implementation for {@link #createAbstractValue(AnnotationMirrorSet, TypeMirror)}. */ + /** + * Default implementation for {@link #createAbstractValue(AnnotationMirrorSet, TypeMirror)}. + * + * @param analysis the analysis + * @param annotations the annotations for the result annotated type + * @param underlyingType the unannotated type for the result annotated type + * @return an abstract value containing the given {@code annotations} and {@code underlyingType} + */ public final @Nullable CFValue defaultCreateAbstractValue( CFAbstractAnalysis analysis, AnnotationMirrorSet annotations, From a6e60b71583ff0583af6f1ea1e86cffcdf6f23e0 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 30 Jul 2025 16:39:23 -0700 Subject: [PATCH 268/374] Remove suppression --- .../dataflow/expression/IteratedCollectionElement.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index 85593588349f..a3f9a381a855 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -28,9 +28,11 @@ public IteratedCollectionElement(Node var, Tree tree) { this.tree = tree; } - @SuppressWarnings("interning:not.interned") @Override public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } if (!(obj instanceof IteratedCollectionElement)) { return false; } From 6f477135a8ab73076bd9d416576b5c432d25a760 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 31 Jul 2025 07:45:21 -0700 Subject: [PATCH 269/374] Add `@NotOwning` annotation --- .../dataflow/cfg/builder/TryFinallyScopeMap.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeMap.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeMap.java index d640a9d6b6ab..af7f930b8243 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeMap.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeMap.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import javax.lang.model.element.Name; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.nullness.qual.Nullable; import org.plumelib.util.ArrayMap; @@ -22,7 +23,7 @@ protected TryFinallyScopeMap() { } @Override - public Label get(@Nullable Object key) { + public @NotOwning Label get(@Nullable Object key) { if (key == null) { throw new IllegalArgumentException(); } From f9da2da8efa554757d4b74f99335d36e17ae6357 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 5 Aug 2025 09:45:08 -0700 Subject: [PATCH 270/374] Suppress warnings --- .../org/checkerframework/common/value/ValueCheckerUtils.java | 1 + .../java/org/checkerframework/framework/stub/StubGenerator.java | 1 + .../org/checkerframework/framework/type/QualifierHierarchy.java | 1 + 3 files changed, 3 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java index bb7ccafde771..37471931dbb0 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueCheckerUtils.java @@ -174,6 +174,7 @@ private static T convertLongToType(long value, Class expectedType) { * @param origValues the objects to format * @return a list of the formatted objects */ + @SuppressWarnings("collectionownership:argument") // defaulting/ownership CollectionsPlume.mapList private static @Nullable List convertToStringVal( List origValues) { if (origValues == null) { diff --git a/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java b/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java index 6fe7ca077d01..098c311717ec 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java @@ -391,6 +391,7 @@ private void indent() { * @param lst a list to format * @return a string representation of the list, without surrounding square brackets */ + @SuppressWarnings("collectionownership:argument") // defaulting/inference for StringsPlume.join private String formatList(@MustCallUnknown List lst) { return StringsPlume.join(", ", lst); } diff --git a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java index 3dd6f7f744c0..6a67d9629afa 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java @@ -779,6 +779,7 @@ public static void assertSameSize(Collection c1, Collection c2) { * @param c2 the second collection * @param result the result collection */ + @SuppressWarnings("collectionownership:argument") // defaulting/inference for StringsPlume.join public static void assertSameSize( @MustCallUnknown Collection c1, @MustCallUnknown Collection c2, From f4d47c6e7517088a48f6e1b87122069f4fd86455 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Tue, 5 Aug 2025 09:47:58 -0700 Subject: [PATCH 271/374] Temporary warning suppression --- .../framework/util/typeinference8/util/Theta.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java index 3ae9182408b5..ae3d6ec5746c 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java @@ -35,6 +35,7 @@ private TypeVariable getTypeVariable(TypeVariable typeVariable) { } @Override + @SuppressWarnings("collectionownership:method.invocation") // TEMPORARY, Sascha to fix public boolean containsKey(Object key) { if (key instanceof TypeVariable) { return super.containsKey(getTypeVariable((TypeVariable) key)); @@ -43,6 +44,10 @@ public boolean containsKey(Object key) { } @Override + @SuppressWarnings({ + "collectionownership:method.invocation", + "builder:owning.override.return" + }) // TEMPORARY, Sascha to fix public Variable get(Object key) { if (key instanceof TypeVariable) { return super.get(getTypeVariable((TypeVariable) key)); From eaa5492065617c8a049b30b24f9827ace548a8c2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 6 Aug 2025 14:18:35 +0200 Subject: [PATCH 272/374] more annotations for rlc4c --- .../framework/util/typeinference8/util/Theta.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java index ae3d6ec5746c..9eb08fb820ae 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference8/util/Theta.java @@ -5,6 +5,8 @@ import java.util.LinkedHashMap; import java.util.List; import javax.lang.model.type.TypeVariable; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.framework.util.typeinference8.types.Variable; import org.checkerframework.javacutil.TypesUtils; @@ -25,7 +27,7 @@ public Theta() {} * @return the type variable in the key set that is {@link TypesUtils#areSame(TypeVariable, * TypeVariable)} as {@code typeVariable} */ - private TypeVariable getTypeVariable(TypeVariable typeVariable) { + private TypeVariable getTypeVariable(@NotOwningCollection Theta this, TypeVariable typeVariable) { for (TypeVariable key : keySet()) { if (TypesUtils.areSame(key, typeVariable)) { return key; @@ -35,7 +37,6 @@ private TypeVariable getTypeVariable(TypeVariable typeVariable) { } @Override - @SuppressWarnings("collectionownership:method.invocation") // TEMPORARY, Sascha to fix public boolean containsKey(Object key) { if (key instanceof TypeVariable) { return super.containsKey(getTypeVariable((TypeVariable) key)); @@ -44,11 +45,7 @@ public boolean containsKey(Object key) { } @Override - @SuppressWarnings({ - "collectionownership:method.invocation", - "builder:owning.override.return" - }) // TEMPORARY, Sascha to fix - public Variable get(Object key) { + public @NotOwning Variable get(Object key) { if (key instanceof TypeVariable) { return super.get(getTypeVariable((TypeVariable) key)); } From e3e06d9bba4839f893468288c57c2588e404979d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 6 Aug 2025 16:47:48 +0200 Subject: [PATCH 273/374] add receiver annotation --- .../framework/test/diagnostics/JavaDiagnosticReader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index ac5c55b1213e..c367c6f36f13 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -12,6 +12,7 @@ import java.util.NoSuchElementException; import javax.tools.JavaFileObject; import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.mustcall.qual.NotOwning; @@ -262,7 +263,8 @@ public void remove() { } @RequiresNonNull("reader") - protected void advance(@UnknownInitialization JavaDiagnosticReader this) throws IOException { + protected void advance(@NotOwningCollection @UnknownInitialization JavaDiagnosticReader this) + throws IOException { nextLine = reader.readLine(); nextLineNumber = reader.getLineNumber(); if (nextLine == null) { From 596f2e19c5d0b507db7e201138c63615a2e83e46 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 6 Aug 2025 17:17:57 +0200 Subject: [PATCH 274/374] add comment --- .../framework/test/diagnostics/JavaDiagnosticReader.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index c367c6f36f13..911a659d9581 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -262,6 +262,12 @@ public void remove() { return codec.createTestDiagnosticLine(filename, currentLine, currentLineNumber); } + /** + * Advances the reader by reading a single line, updating the {@code nextLine} and {@code + * NextLineNumber} fields. If there is no remaining line, the reader is {@code close}d. + * + * @param this the receiver + */ @RequiresNonNull("reader") protected void advance(@NotOwningCollection @UnknownInitialization JavaDiagnosticReader this) throws IOException { From 6b17e65b1195001da757da27610bf417aaa85c0f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 6 Aug 2025 18:43:33 +0200 Subject: [PATCH 275/374] add rlc4c annos --- .../test/diagnostics/JavaDiagnosticReader.java | 2 -- .../javacutil/AnnotationMirrorMap.java | 5 +++-- .../javacutil/AnnotationMirrorSet.java | 15 +++++++++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index 911a659d9581..bb473649a637 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -265,8 +265,6 @@ public void remove() { /** * Advances the reader by reading a single line, updating the {@code nextLine} and {@code * NextLineNumber} fields. If there is no remaining line, the reader is {@code close}d. - * - * @param this the receiver */ @RequiresNonNull("reader") protected void advance(@NotOwningCollection @UnknownInitialization JavaDiagnosticReader this) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java index 33a449068985..88f58879729e 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java @@ -7,6 +7,7 @@ import java.util.Set; import java.util.TreeMap; import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.returnsreceiver.qual.This; @@ -112,7 +113,7 @@ public boolean containsValue(Object value) { @Override @Pure - public @Nullable V get(Object key) { + public @NotOwning @Nullable V get(Object key) { if (key instanceof AnnotationMirror) { AnnotationMirror keyAnno = AnnotationUtils.getSame(shadowMap.keySet(), (AnnotationMirror) key); @@ -129,7 +130,7 @@ public boolean containsValue(Object value) { "keyfor:argument" }) // delegation @Override - public @Nullable V put(AnnotationMirror key, V value) { + public @NotOwning @Nullable V put(AnnotationMirror key, V value) { V pre = get(key); remove(key); shadowMap.put(key, value); diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java index cc0bd51264f1..29e887a5aebd 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java @@ -7,7 +7,10 @@ import java.util.NavigableSet; import java.util.TreeSet; import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.KeyForBottom; import org.checkerframework.checker.nullness.qual.Nullable; @@ -124,7 +127,6 @@ public static AnnotationMirrorSet emptySet() { // Set methods @Override - @SuppressWarnings("collectionownership:override.receiver") public int size() { return shadowSet.size(); } @@ -143,7 +145,8 @@ public boolean contains( } @Override - public Iterator<@KeyFor("this") AnnotationMirror> iterator() { + public Iterator<@KeyFor("this") AnnotationMirror> iterator( + @PolyOwningCollection AnnotationMirrorSet this) { return shadowSet.iterator(); } @@ -159,13 +162,12 @@ public Object[] toArray() { } @SuppressWarnings({ - "keyfor:argument", // delegation - "collectionownership:override.receiver" + "keyfor:argument" // delegation }) @Override public boolean add( @UnknownInitialization(AnnotationMirrorSet.class) AnnotationMirrorSet this, - AnnotationMirror annotationMirror) { + @Owning AnnotationMirror annotationMirror) { if (contains(annotationMirror)) { return false; } @@ -183,7 +185,7 @@ public boolean remove(@Nullable Object o) { } @Override - public boolean containsAll(Collection c) { + public boolean containsAll(@NotOwningCollection AnnotationMirrorSet this, Collection c) { for (Object o : c) { if (!contains(o)) { return false; @@ -260,6 +262,7 @@ public boolean equals(@Nullable Object o) { } @Override + @SuppressWarnings("collectionownership:method.invocation") // iterator() requires polyoc receiver public int hashCode() { int result = 0; Iterator i = iterator(); From dada8a3b7f71bc3a6c765cfa53f00f3a916d3641 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 6 Aug 2025 18:58:32 +0200 Subject: [PATCH 276/374] fix javadoc --- .../framework/test/diagnostics/JavaDiagnosticReader.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index bb473649a637..ef72856bedb7 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -265,6 +265,8 @@ public void remove() { /** * Advances the reader by reading a single line, updating the {@code nextLine} and {@code * NextLineNumber} fields. If there is no remaining line, the reader is {@code close}d. + * + * @throws IOException propagated reader exception */ @RequiresNonNull("reader") protected void advance(@NotOwningCollection @UnknownInitialization JavaDiagnosticReader this) From 7bcf851c461a8c1e78d0fb46e1a6fbba27eb8bd7 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Thu, 7 Aug 2025 10:33:05 +0200 Subject: [PATCH 277/374] remove unnecessary warning suppression --- .../java/org/checkerframework/javacutil/AnnotationMirrorSet.java | 1 - 1 file changed, 1 deletion(-) diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java index 29e887a5aebd..a334530895ed 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java @@ -262,7 +262,6 @@ public boolean equals(@Nullable Object o) { } @Override - @SuppressWarnings("collectionownership:method.invocation") // iterator() requires polyoc receiver public int hashCode() { int result = 0; Iterator i = iterator(); From 343f76aca4965b143d742dcc65daa81b1f73cb7c Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Aug 2025 09:48:14 -0700 Subject: [PATCH 278/374] Add annotations --- .../scenelib/io/classfile/ClassAnnotationSceneWriter.java | 5 +++-- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java index 48d25fca072c..34a49d02e7a7 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java @@ -27,6 +27,7 @@ import org.checkerframework.afu.scenelib.field.ArrayAFT; import org.checkerframework.afu.scenelib.field.ClassTokenAFT; import org.checkerframework.afu.scenelib.field.EnumAFT; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; @@ -430,7 +431,7 @@ public FieldAnnotationSceneWriter(int api, String name, FieldVisitor fv) { } @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + public @Nullable AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { existingFieldAnnotations.add(descriptor); // If annotation exists in scene, and in overwrite mode, @@ -443,7 +444,7 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { } @Override - public AnnotationVisitor visitTypeAnnotation( + public @Nullable AnnotationVisitor visitTypeAnnotation( int typeRef, TypePath typePath, String descriptor, boolean visible) { // typeRef: FIELD existingFieldAnnotations.add(descriptor); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 61e270b17397..23690ad90b0e 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -4,7 +4,6 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.Map; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; @@ -15,10 +14,14 @@ * java.util.LinkedHashMap} and its {@link java.util.LinkedHashMap#values() value collection}. */ public class LinkedHashKeyedSet extends AbstractSet implements KeyedSet { + /** Produces a key for a value. */ private final Keyer keyer; - private final Map theMap = new LinkedHashMap<>(); + /** The map that backs this set. */ + // Declared as LinkedHashMap because some implementations of Map prohibit null keys. + private final LinkedHashMap theMap = new LinkedHashMap<>(); + /** The values in the set. Implemented as a view into the map. */ final Collection theValues = theMap.values(); /** From 7f51f70d1ebeaf379ded4c9cb4ef072009340955 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Aug 2025 10:30:06 -0700 Subject: [PATCH 279/374] Add Javadoc --- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 23690ad90b0e..18ea4b261f70 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -12,6 +12,9 @@ /** * A simple implementation of {@link KeyedSet} backed by an insertion-order {@link * java.util.LinkedHashMap} and its {@link java.util.LinkedHashMap#values() value collection}. + * + * @param the type of keys + * @param the type of values */ public class LinkedHashKeyedSet extends AbstractSet implements KeyedSet { /** Produces a key for a value. */ From 19f39a8e4abcf24932d202f13e4033a6a36b9e2d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Aug 2025 10:45:20 -0700 Subject: [PATCH 280/374] Add annotations, suppress warnings --- .../scenelib/io/classfile/ClassAnnotationSceneWriter.java | 2 ++ .../checkerframework/afu/scenelib/util/coll/WrapperMap.java | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java index 34a49d02e7a7..b3c2ba9f7bf9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java @@ -431,6 +431,7 @@ public FieldAnnotationSceneWriter(int api, String name, FieldVisitor fv) { } @Override + @SuppressWarnings("nullness:override.return") // ASM lacks (some?) @Nullable annotations public @Nullable AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { existingFieldAnnotations.add(descriptor); @@ -444,6 +445,7 @@ public FieldAnnotationSceneWriter(int api, String name, FieldVisitor fv) { } @Override + @SuppressWarnings("nullness:override.return") // ASM lacks (some?) @Nullable annotations public @Nullable AnnotationVisitor visitTypeAnnotation( int typeRef, TypePath typePath, String descriptor, boolean visible) { // typeRef: FIELD diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index a41c8982024d..2e279a97207d 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -40,7 +40,7 @@ public Set> entrySet() { } @Override - public @NotOwning V get(Object key) { + public @Nullable @NotOwning V get(Object key) { return back.get(key); } @@ -55,7 +55,8 @@ public Set keySet() { } @Override - public @NotOwning V put(K key, V value) { + @SuppressWarnings("keyfor:contracts.postcondition") // uses a delegate map + public @Nullable @NotOwning V put(K key, V value) { return back.put(key, value); } From 8e2fc869cb16a58979489133ce705e527d7652d1 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Aug 2025 10:53:45 -0700 Subject: [PATCH 281/374] import `@Nullable` --- .../org/checkerframework/afu/scenelib/util/coll/WrapperMap.java | 1 + 1 file changed, 1 insertion(+) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index 2e279a97207d..d70a6f3bd091 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Set; import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A {@link WrapperMap} is a map all of whose methods delegate by default to those of a supplied From 1480bedf73eea32e5af242275c94bbc6837a4e29 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 7 Aug 2025 11:02:47 -0700 Subject: [PATCH 282/374] Move annotations --- .../checkerframework/afu/scenelib/util/coll/WrapperMap.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java index d70a6f3bd091..b826e9861bc1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/WrapperMap.java @@ -41,7 +41,7 @@ public Set> entrySet() { } @Override - public @Nullable @NotOwning V get(Object key) { + public @NotOwning @Nullable V get(Object key) { return back.get(key); } @@ -57,7 +57,7 @@ public Set keySet() { @Override @SuppressWarnings("keyfor:contracts.postcondition") // uses a delegate map - public @Nullable @NotOwning V put(K key, V value) { + public @NotOwning @Nullable V put(K key, V value) { return back.put(key, value); } From 251cb9478658a4289bb61a39ddc3bf874fdcf08a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 12:00:52 +0200 Subject: [PATCH 283/374] address code review --- .../expression/IteratedCollectionElement.java | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index a3f9a381a855..4376fbb98e1b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -1,6 +1,7 @@ package org.checkerframework.dataflow.expression; import com.sun.source.tree.Tree; +import java.util.Objects; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.AnnotationProvider; @@ -37,52 +38,14 @@ public boolean equals(@Nullable Object obj) { return false; } IteratedCollectionElement other = (IteratedCollectionElement) obj; - return other.tree.equals(this.tree) || other.node.equals(this.node); + return other.tree.equals(this.tree) && other.node.equals(this.node); } - // /** - // * Returns true if the two elements are the same. - // * - // * @param element1 the first element to compare - // * @param element2 the second element to compare - // * @return true if the two elements are the same - // */ - // protected static boolean sameElement(VariableElement element1, VariableElement element2) { - // VarSymbol vs1 = (VarSymbol) element1; - // VarSymbol vs2 = (VarSymbol) element2; - // // If a LocalVariable is created via JavaExpressionParseUtil#parse, then `vs1.equals(vs2)` - // // will not return true even if the elements represent the same local variable. - // // The owner of a lambda parameter is the enclosing method, so a local variable and a lambda - // // parameter might have the same name and the same owner. Use pos to differentiate this - // // case. - // return vs1.pos == vs2.pos && vs1.name == vs2.name && vs1.owner.equals(vs2.owner); - // } - - // /** - // * Returns the element for this variable. - // * - // * @return the element for this variable - // */ - // public VariableElement getElement() { - // return element; - // } - @Override public int hashCode() { - return node.hashCode(); - // return Objects.hash(tree.hashCode()); + return Objects.hash(tree, node); } - // @Override - // public String toString() { - // return var.toString(); - // } - - // @Override - // public String toStringDebug() { - // return super.toStringDebug() + " [owner=" + ((VarSymbol) element).owner + "]"; - // } - @SuppressWarnings("unchecked") // generic cast @Override public @Nullable T containedOfClass(Class clazz) { @@ -116,7 +79,6 @@ public boolean isAssignableByOtherCode() { @Override public boolean isModifiableByOtherCode() { return false; - // return !TypesUtils.isImmutableTypeInJdk(((VarSymbol) element).type); } @Override From 02cc724b8c4b1c6caa3507957f32fd937ecec55d Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 12:12:10 +0200 Subject: [PATCH 284/374] address code review: remove createSourceVisitor override in coc --- .../collectionownership/CollectionOwnershipChecker.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index 28dbda33f176..217d62991802 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -3,7 +3,6 @@ import java.util.Set; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.source.SourceChecker; /** @@ -16,12 +15,6 @@ public class CollectionOwnershipChecker extends BaseTypeChecker { /** Creates a CollectionOwnershipChecker. */ public CollectionOwnershipChecker() {} - // TODO sck: not sure if this is necessary - @Override - protected BaseTypeVisitor createSourceVisitor() { - return new CollectionOwnershipVisitor(this); - } - @Override protected Set> getImmediateSubcheckerClasses() { Set> checkers = super.getImmediateSubcheckerClasses(); From 72bdbc8946620ef68d16d4f82f2619bdfd191e4b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 12:32:33 +0200 Subject: [PATCH 285/374] remove unnecessary nullness checks --- ...llectionOwnershipAnnotatedTypeFactory.java | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 75bb5a742413..49cf779ae8df 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.collectionownership; -import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -31,7 +30,6 @@ import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; @@ -181,11 +179,6 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { this.postInit(); } - @Override - public void setRoot(@Nullable CompilationUnitTree newRoot) { - super.setRoot(newRoot); - } - @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>( @@ -248,9 +241,6 @@ public void postAnalyze(ControlFlowGraph cfg) { * @return true if t is a resource collection */ public boolean isResourceCollection(TypeMirror t) { - if (t == null) { - return false; - } List mcValues = getMustCallValuesOfResourceCollectionComponent(t); return mcValues != null && mcValues.size() > 0; } @@ -295,9 +285,6 @@ public boolean isOwningCollectionField(Element elt) { * @return true if the element is a resource collection field */ public boolean isResourceCollectionField(Element elt) { - if (elt == null) { - return false; - } if (elt.getKind().isField()) { if (isResourceCollection(elt.asType())) { return true; @@ -316,9 +303,6 @@ public boolean isResourceCollectionField(Element elt) { * {@code @OwningCollection} by declaration */ public boolean isOwningCollectionParameter(Element elt) { - if (elt == null) { - return false; - } if (isResourceCollection(elt.asType())) { if (elt.getKind() == ElementKind.PARAMETER) { AnnotatedTypeMirror atm = getAnnotatedType(elt); @@ -348,9 +332,6 @@ public boolean isOwningCollectionParameter(Element elt) { * @return true if the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { - if (tree == null) { - return false; - } MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); AnnotatedTypeMirror treeMcType = null; try { @@ -374,9 +355,6 @@ public boolean isResourceCollection(Tree tree) { * if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedTypeMirror atm) { - if (atm == null) { - return null; - } boolean isCollectionType = ResourceLeakUtils.isCollection(atm.getUnderlyingType()); AnnotatedTypeMirror componentType = null; @@ -425,9 +403,6 @@ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { * if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) { - if (t == null) { - return null; - } boolean isCollectionType = ResourceLeakUtils.isCollection(t); TypeMirror componentType = null; @@ -495,9 +470,6 @@ public CollectionOwnershipType getCoType(Tree tree) { * @return the extracted {@code CollectionOwnershipType} from annos */ public CollectionOwnershipType getCoType(Collection annos) { - if (annos == null) { - return null; - } for (AnnotationMirror anm : annos) { if (anm == null) { continue; From d552b3e5d516366644b781d294efd55d1b3478fe Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 12:48:54 +0200 Subject: [PATCH 286/374] make mcatf a field in coatf --- .../CollectionOwnershipAnnotatedTypeFactory.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 49cf779ae8df..b51474cdcbad 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -68,6 +68,12 @@ public class CollectionOwnershipAnnotatedTypeFactory CollectionOwnershipTransfer, CollectionOwnershipAnalysis> { + /** + * The {@code @} {@link MustCallAnnotatedTypeFactory} instance in the checker hierarchy. Used for + * getting the {@code @MustCall} type of expressions. + */ + private final MustCallAnnotatedTypeFactory mcAtf; + /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -176,6 +182,7 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); POLY = AnnotationBuilder.fromClass(elements, PolyOwningCollection.class); + mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); this.postInit(); } @@ -332,7 +339,6 @@ public boolean isOwningCollectionParameter(Element elt) { * @return true if the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { - MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); AnnotatedTypeMirror treeMcType = null; try { treeMcType = mcAtf.getAnnotatedType(tree); @@ -367,7 +373,6 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType } if (componentType != null) { - MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); return list; } else { @@ -387,7 +392,6 @@ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedType * or null if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(Tree tree) { - MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); return getMustCallValuesOfResourceCollectionComponent(mcAtf.getAnnotatedType(tree)); } @@ -414,7 +418,6 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) } if (componentType != null) { - MustCallAnnotatedTypeFactory mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(this); List list = ResourceLeakUtils.getMcValues(componentType, mcAtf); return list; } else { From aab1708464e4c1a68eca3cb1d5d950758f6525c3 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 13:01:50 +0200 Subject: [PATCH 287/374] remove catch bugincf to make error explicit --- .../CollectionOwnershipAnnotatedTypeFactory.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index b51474cdcbad..15c0306a0f4b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -56,7 +56,6 @@ import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; @@ -339,12 +338,10 @@ public boolean isOwningCollectionParameter(Element elt) { * @return true if the tree is a resource collection */ public boolean isResourceCollection(Tree tree) { - AnnotatedTypeMirror treeMcType = null; - try { - treeMcType = mcAtf.getAnnotatedType(tree); - } catch (BugInCF e) { + if (tree == null) { return false; } + AnnotatedTypeMirror treeMcType = mcAtf.getAnnotatedType(tree); List mcValues = getMustCallValuesOfResourceCollectionComponent(treeMcType); return mcValues != null && mcValues.size() > 0; } From bba3c2fd67b8cb470f2c178613e5810406009f20 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 14:12:09 +0200 Subject: [PATCH 288/374] remove mcoeobligationaltering loop wrapper class --- .../CollectionOwnershipTransfer.java | 2 +- .../MustCallConsistencyAnalyzer.java | 4 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 106 +++++------------- 3 files changed, 32 insertions(+), 80 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 1ce16d924fd8..afce54d68b41 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -136,7 +136,7 @@ private TransferResult transformPotentiallyFu if (collectionCoType == CollectionOwnershipType.OwningCollection) { List mustCallValuesOfElements = atypeFactory.getMustCallValuesOfResourceCollectionComponent(loop.collectionTree); - if (loop.getMethods().containsAll(mustCallValuesOfElements)) { + if (loop.getCalledMethods().containsAll(mustCallValuesOfElements)) { elseStore.clearValue(collectionJE); elseStore.insertValue(collectionJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); return new ConditionalTransferResult<>( diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index af1cbe97de52..abef990c1304 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2572,7 +2572,7 @@ private void propagateObligationsToSuccessorBlock( if (isElseEdgeOfFulfillingLoop) { if (obligation instanceof CollectionObligation) { String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; - if (loop.getMethods().contains(mustCallMethodOfCo)) { + if (loop.getCalledMethods().contains(mustCallMethodOfCo)) { // don't propagate this obligation along this edge, as it was fulfilled // in the loop that the currentBlock is the conditional block of continue; @@ -3527,7 +3527,7 @@ public void analyzeObligationFulfillingLoop( // now put the loop into the static datastructure if it calls any methods on the element if (calledMethodsInLoop != null && calledMethodsInLoop.size() > 0) { - potentiallyFulfillingLoop.addMethods(calledMethodsInLoop); + potentiallyFulfillingLoop.addCalledMethods(calledMethodsInLoop); CollectionOwnershipAnnotatedTypeFactory.markFulfillingLoop(potentiallyFulfillingLoop); } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 6baf9ecd89cf..9d34792e0995 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -626,13 +626,8 @@ public TransferInput getInput(Block block) } } - /** Wrapper class for a loop that might have an effect on the obligation of a collection/array. */ - public abstract static class CollectionObligationAlteringLoop { - /** Loop is fulfilling. */ - public static enum LoopKind { - /** Loop potentially calls methods on all elements of a collection. */ - FULFILLING - } + /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ + public static class PotentiallyFulfillingLoop { /** AST {@code Tree} for collection iterated over. */ public final ExpressionTree collectionTree; @@ -644,71 +639,10 @@ public static enum LoopKind { public final Tree condition; /** - * methods associated with this loop. For assigning loops, these are methods that are to be - * added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to be - * removed from the {@code MustCallOnElements} and added to the {@code CalledMethodsOnElements} - * type. - */ - protected final Set associatedMethods; - - /** - * Wether loop is assigning (elements with {@code MustCall} obligations to a collection) or - * fulfilling. - */ - public final LoopKind loopKind; - - /** - * Constructs a new {@code CollectionObligationAlteringLoop}. Called by subclass constructor. - * - * @param collectionTree AST {@code Tree} for collection iterated over - * @param collectionElementTree AST {@code Tree} for collection element iterated over - * @param condition AST {@code Tree} for loop condition - * @param associatedMethods set of methods associated with this loop - * @param loopKind the type of loop, e.g., assigning/fulfilling + * The methods that the loop definitely calls on all elements of the collection it iterates + * over. */ - protected CollectionObligationAlteringLoop( - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree condition, - Set associatedMethods, - LoopKind loopKind) { - this.collectionTree = collectionTree; - this.collectionElementTree = collectionElementTree; - this.condition = condition; - this.loopKind = loopKind; - if (associatedMethods == null) { - associatedMethods = new HashSet<>(); - } - this.associatedMethods = associatedMethods; - } - - /** - * Add methods associated with this loop. For assigning loops, these are methods that are to be - * added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to be - * removed from the {@code MustCallOnElements} and added to the {@code CalledMethodsOnElements} - * type. - * - * @param methods the set of methods to add - */ - public void addMethods(Set methods) { - associatedMethods.addAll(methods); - } - - /** - * Returns methods associated with this loop. For assigning loops, these are methods that are to - * be added to the {@code MustCallOnElements} type and for fulfilling loops, methods that are to - * be removed from the {@code MustCallOnElements} and added to the {@code - * CalledMethodsOnElements} type. - * - * @return the set of associated methdos - */ - public Set getMethods() { - return associatedMethods; - } - } - - /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ - public static class PotentiallyFulfillingLoop extends CollectionObligationAlteringLoop { + protected final Set calledMethods; /** cfg {@code Block} containing the loop body entry. */ public final Block loopBodyEntryBlock; @@ -741,17 +675,35 @@ public PotentiallyFulfillingLoop( Block loopUpdateBlock, ConditionalBlock loopConditionalBlock, Node collectionEltNode) { - super( - collectionTree, - collectionElementTree, - condition, - new HashSet<>(), - CollectionObligationAlteringLoop.LoopKind.FULFILLING); + this.collectionTree = collectionTree; + this.collectionElementTree = collectionElementTree; + this.condition = condition; + this.calledMethods = new HashSet<>(); this.loopBodyEntryBlock = loopBodyEntryBlock; this.loopUpdateBlock = loopUpdateBlock; this.loopConditionalBlock = loopConditionalBlock; this.collectionElementNode = collectionEltNode; } + + /** + * Add methods that are guaranteed to be invoked on every element of the collection the loop + * iterates over. + * + * @param methods the set of methods to add + */ + public void addCalledMethods(Set methods) { + calledMethods.addAll(methods); + } + + /** + * Returns methods that are guaranteed to be invoked on every element of the collection the loop + * iterates over. + * + * @return the set of methods the loop calls on all elements of the iterated collection + */ + public Set getCalledMethods() { + return calledMethods; + } } /** From d915819fb493088ada83fa7fc98a1956d9dc5708 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 14:26:53 +0200 Subject: [PATCH 289/374] replace catch (Exception) with catch (BugInCF) --- .../CollectionOwnershipAnnotatedTypeFactory.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 15c0306a0f4b..dd4a2926f357 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -56,6 +56,7 @@ import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; @@ -431,13 +432,17 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) * @return the {@code CollectionOwnershipType} that the given node has in the given store */ public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coStore) { + JavaExpression jx = JavaExpression.fromNode(node); + CFValue storeVal; try { - JavaExpression jx = JavaExpression.fromNode(node); - CFValue storeVal = coStore.getValue(jx); - return getCoType(storeVal.getAnnotations()); - } catch (Exception e) { + storeVal = coStore.getValue(jx); + } catch (BugInCF e) { + storeVal = null; + } + if (storeVal == null) { return null; } + return getCoType(storeVal.getAnnotations()); } /** From b88929c23a0ec805b79625e278717fb6cb18e06f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 15:18:35 +0200 Subject: [PATCH 290/374] minor changes code review --- ...llectionOwnershipAnnotatedTypeFactory.java | 51 +++++++++---------- .../MustCallConsistencyAnalyzer.java | 2 +- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index dd4a2926f357..ba077ae30051 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -269,7 +269,7 @@ public boolean isOwningCollectionField(Element elt) { if (isResourceCollection(elt.asType())) { AnnotatedTypeMirror atm = getAnnotatedType(elt); CollectionOwnershipType fieldType = - getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + getCoType(Collections.singletonList(atm.getPrimaryAnnotationInHierarchy(TOP))); if (fieldType == null) { return false; } @@ -314,7 +314,7 @@ public boolean isOwningCollectionParameter(Element elt) { if (elt.getKind() == ElementKind.PARAMETER) { AnnotatedTypeMirror atm = getAnnotatedType(elt); CollectionOwnershipType paramType = - getCoType(Collections.singletonList(atm.getEffectiveAnnotationInHierarchy(TOP))); + getCoType(Collections.singletonList(atm.getPrimaryAnnotationInHierarchy(TOP))); if (paramType == null) { return false; } @@ -474,7 +474,7 @@ public CollectionOwnershipType getCoType(Tree tree) { * @param annos the {@code AnnotationMirror} collection * @return the extracted {@code CollectionOwnershipType} from annos */ - public CollectionOwnershipType getCoType(Collection annos) { + public CollectionOwnershipType getCoType(Collection annos) { for (AnnotationMirror anm : annos) { if (anm == null) { continue; @@ -578,12 +578,12 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { AnnotatedDeclaredType receiverType = t.getReceiverType(); AnnotationMirror receiverAnno = receiverType == null ? null : receiverType.getEffectiveAnnotationInHierarchy(TOP); - boolean receiverHasManualAnno = + boolean receiverHasExplicitAnno = receiverAnno != null && !AnnotationUtils.areSameByName(BOTTOM, receiverAnno); AnnotatedTypeMirror returnType = t.getReturnType(); AnnotationMirror returnAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); - boolean returnHasManualAnno = + boolean returnHasExplicitAnno = returnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, returnAnno); // inherit supertype annotations @@ -597,26 +597,26 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { AnnotatedExecutableType annotatedSuperMethod = CollectionOwnershipAnnotatedTypeFactory.this.getAnnotatedType(superElt); - if (!receiverHasManualAnno) { + if (!receiverHasExplicitAnno) { AnnotatedDeclaredType superReceiver = annotatedSuperMethod.getReceiverType(); AnnotationMirror superReceiverAnno = superReceiver.getPrimaryAnnotationInHierarchy(TOP); - boolean superReceiverHasManualAnno = + boolean superReceiverHasExplicitAnno = superReceiverAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superReceiverAnno) && !AnnotationUtils.areSameByName(POLY, superReceiverAnno); - if (superReceiverHasManualAnno) { + if (superReceiverHasExplicitAnno) { receiverType.replaceAnnotation(superReceiverAnno); } } - if (!returnHasManualAnno) { + if (!returnHasExplicitAnno) { AnnotatedTypeMirror superReturnType = annotatedSuperMethod.getReturnType(); AnnotationMirror superReturnAnno = superReturnType.getPrimaryAnnotationInHierarchy(TOP); - boolean superReturnHasManualAnno = + boolean superReturnHasExplicitAnno = superReturnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, superReturnAnno) && !AnnotationUtils.areSameByName(POLY, superReturnAnno); - if (superReturnHasManualAnno) { + if (superReturnHasExplicitAnno) { returnType.replaceAnnotation(superReturnAnno); } } @@ -624,21 +624,19 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { List paramTypes = t.getParameterTypes(); List superParamTypes = annotatedSuperMethod.getParameterTypes(); - if (paramTypes.size() == superParamTypes.size()) { - for (int i = 0; i < superParamTypes.size(); i++) { - AnnotationMirror paramAnno = paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP); - boolean paramHasManualAnno = - paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); - if (!paramHasManualAnno) { - AnnotationMirror superParamAnno = - superParamTypes.get(i).getPrimaryAnnotationInHierarchy(TOP); - boolean superParamHasManualAnno = - superParamAnno != null - && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno) - && !AnnotationUtils.areSameByName(POLY, superParamAnno); - if (superParamHasManualAnno) { - paramTypes.get(i).replaceAnnotation(superParamAnno); - } + for (int i = 0; i < superParamTypes.size(); i++) { + AnnotationMirror paramAnno = paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP); + boolean paramHasExplicitAnno = + paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); + if (!paramHasExplicitAnno) { + AnnotationMirror superParamAnno = + superParamTypes.get(i).getPrimaryAnnotationInHierarchy(TOP); + boolean superParamHasExplicitAnno = + superParamAnno != null + && !AnnotationUtils.areSameByName(BOTTOM, superParamAnno) + && !AnnotationUtils.areSameByName(POLY, superParamAnno); + if (superParamHasExplicitAnno) { + paramTypes.get(i).replaceAnnotation(superParamAnno); } } } @@ -723,6 +721,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { for (int i = 0; i < params.size(); i++) { if (params.get(i).getSimpleName() == elt.getSimpleName()) { type.replaceAnnotation(paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP)); + break; } } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index abef990c1304..d43a76e5212b 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1384,7 +1384,7 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg, ReturnNode nod MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); boolean returnTypeHasManualNocAnno = - coAtf.getCoType(new HashSet<>(executableElement.getReturnType().getAnnotationMirrors())) + coAtf.getCoType(executableElement.getReturnType().getAnnotationMirrors()) == CollectionOwnershipType.NotOwningCollection; if (returnTypeHasManualNocAnno) { // return is @NotOwningCollection. No ownership transfer to call-site. From 0ab02c8d3913d5fa8f2e2adc9bb68384ab6d8c4f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 15:48:13 +0200 Subject: [PATCH 291/374] minor changes code review --- .../CollectionOwnershipVisitor.java | 16 +++++++-------- .../collectionownership/messages.properties | 1 + .../MustCallConsistencyAnalyzer.java | 20 +++++++++++++++++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index b5e0a45c0cf8..c61b1f42271d 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -9,6 +9,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.VariableElement; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -52,9 +53,10 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { * the class, if one exists. * *

This method typically issues a warning if the result type of the constructor is not top, - * because in top-default type systems that indicates a potential problem. The Must Call Checker - * does not need this warning, because it expects the type of all constructors to be {@code - * OwningCollectionBottom} (by default). + * because in top-default type systems that indicates a potential problem. For the Collection + * Ownership Checker, this does not apply, since the default return type is bottom. This is + * justified by examining all return types and setting them to a safe default in {@code + * CollectionOwnershipTypeAnnotator#visitExecutable}. * * @param constructorType an AnnotatedExecutableType for the constructor * @param constructorElement element that declares the constructor @@ -158,11 +160,9 @@ private void checkOwningCollectionField(VariableTree fieldTree) { } checker.reportError( fieldTree, - "unfulfilled.collection.obligations", - mustCallValues.get(0).equals(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME) - ? "Unknown" - : mustCallValues.get(0), - "field " + fieldTree.getName().toString(), + "unfulfilled.field.obligations", + fieldTree.getName().toString(), + MustCallConsistencyAnalyzer.formatMissingMustCallMethods(mustCallValues), error); } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index c2d226ef01c1..f5ed712d0849 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -1,4 +1,5 @@ unfulfilled.collection.obligations=@MustCall method %s may not have been invoked on the elements of %s.%nReason for going out of scope: %s +unfulfilled.field.obligations=The field %s may not have all of its @MustCall %s called on its elements.%nReason for failure of proving fulfillment: %s transfer.owningcollection.field.ownership=Method invocation unsafely transfers the ownership away from field %s. An @OwningCollection field can never lose ownership. illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index d43a76e5212b..63698852c3e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3163,12 +3163,28 @@ public static String formatMissingMustCallMethods(List mustCallVal) { if (size == 0) { throw new TypeSystemError("empty mustCallVal " + mustCallVal); } else if (size == 1) { - return "method " + mustCallVal.get(0); + return "method " + formatMustCallMethod(mustCallVal.get(0)); } else { - return "methods " + String.join(", ", mustCallVal); + return "methods " + + String.join(", ", mustCallVal.stream().map(s -> formatMustCallMethod(s)).toList()); } } + /** + * Returns a human-readable representation of a must-call method name. If the given method name + * equals {@link CollectionOwnershipAnnotatedTypeFactory#UNKNOWN_METHOD_NAME}, the literal string + * {@code "Unknown"} is returned. Otherwise, the original name is returned unchanged. + * + * @param method the must-call method name + * @return {@code "Unknown"} if {@code method} equals {@code UNKNOWN_METHOD_NAME}, otherwise + * {@code method} itself + */ + private static String formatMustCallMethod(String method) { + return method.equals(CollectionOwnershipAnnotatedTypeFactory.UNKNOWN_METHOD_NAME) + ? "Unknown" + : method; + } + /** * A pair of a {@link Block} and a set of dataflow facts on entry to the block. Each dataflow fact * represents a set of resource aliases for some tracked resource. The analyzer's worklist From 03aed4a8adf9c0451a655f0b31e6870691d4a7dc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 16:40:43 +0200 Subject: [PATCH 292/374] fix expected test outcome after renaming an error code --- .../resourceleak-collections/OwningCollectionFieldTest.java | 2 +- .../resourceleak-collections/OwningCollectionFieldTyping.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java index 1c3ad4417e05..a18fb2084b85 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -57,7 +57,7 @@ public void partialClose() { // has no @MustCall method class IllegalAggregator { - // :: error: unfulfilled.collection.obligations + // :: error: unfulfilled.field.obligations List resList = new ArrayList<>(); @CollectionFieldDestructor("resList") diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 22b6396e0d41..650d7fe314b1 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -104,7 +104,7 @@ public void close() { } class OwningCollectionFieldTyping { - // :: error: unfulfilled.collection.obligations + // :: error: unfulfilled.field.obligations List ocField = new ArrayList<>(); void tryTransferringFieldOwnershipAssignment() { From 3bf63c484a63573f1de6a237ae9d10aaee11c751 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 16:41:16 +0200 Subject: [PATCH 293/374] minor fixes code review --- ...llectionOwnershipAnnotatedTypeFactory.java | 2 +- .../CollectionOwnershipTransfer.java | 24 ++----------------- .../CollectionOwnershipVisitor.java | 2 +- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ba077ae30051..089bf61853a0 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -669,7 +669,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { } /* - * Defaults resource collection fields within methods of the class to @OwningCollection. + * Defaults resource collection field uses within member methods to @OwningCollection. */ @Override protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean iUseFlow) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index afce54d68b41..21b6aed368c6 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -218,7 +218,7 @@ private TransferResult transferOwnershipForMe CollectionOwnershipType argType = atypeFactory.getCoType(arg, atypeFactory.getStoreBefore(node)); CollectionOwnershipType paramType = - atypeFactory.getCoType(new HashSet<>(param.asType().getAnnotationMirrors())); + atypeFactory.getCoType(param.asType().getAnnotationMirrors()); if (paramType == null) { continue; } @@ -313,31 +313,11 @@ public void updateStoreWithTempVar( atypeFactory .getAnnotatedType(node.getTree()) .getPrimaryAnnotationInHierarchy(atypeFactory.TOP); - insertInStores(result, localExp, anm == null ? atypeFactory.TOP : anm); + insertIntoStores(result, localExp, anm == null ? atypeFactory.TOP : anm); } } } - /** - * Inserts {@code newAnno} as the value into all stores (conditional or not) in the result for - * node. - * - * @param result the TransferResult holding the stores to modify - * @param target the receiver whose value should be modified - * @param newAnno the new value - */ - protected static void insertInStores( - TransferResult result, - JavaExpression target, - AnnotationMirror newAnno) { - if (result.containsTwoStores()) { - result.getThenStore().insertValue(target, newAnno); - result.getElseStore().insertValue(target, newAnno); - } else { - result.getRegularStore().insertValue(target, newAnno); - } - } - /** * Inserts {@code newAnno} as the value into all stores (conditional or not) in the result for * node, clearing the previous value first. diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index c61b1f42271d..0d97e6269b0f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -77,7 +77,7 @@ protected void checkConstructorResult( } /** - * Change the default for exception parameter lower bounds to bottom (the default), to prevent + * Change the default for exception parameter lower bounds to bottom, to prevent * false positives. * * @return a set containing only the Bottom annotation From 8ed76d7a240aedfcb72232c6b9985f6d296a9a0a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 16:41:47 +0200 Subject: [PATCH 294/374] formatting --- .../collectionownership/CollectionOwnershipTransfer.java | 1 - .../collectionownership/CollectionOwnershipVisitor.java | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 21b6aed368c6..3efaa40e46fa 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.Tree; -import java.util.HashSet; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 0d97e6269b0f..c872f51b652f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -77,8 +77,7 @@ protected void checkConstructorResult( } /** - * Change the default for exception parameter lower bounds to bottom, to prevent - * false positives. + * Change the default for exception parameter lower bounds to bottom, to prevent false positives. * * @return a set containing only the Bottom annotation */ From d00a61516180f06a2a6660c6eb46cf6cafcaba98 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 17:31:41 +0200 Subject: [PATCH 295/374] minor changes code review --- .../CollectionOwnershipTransfer.java | 6 +-- .../MustCallAnnotatedTypeFactory.java | 28 +++++------ .../checker/mustcall/MustCallVisitor.java | 47 +++++++------------ .../MustCallConsistencyAnalyzer.java | 2 +- 4 files changed, 33 insertions(+), 50 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 3efaa40e46fa..1fe5ed59adce 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -123,7 +123,7 @@ public TransferResult visitAssignment( * collection-obligation-fulfilling loop * @return the resulting transfer result */ - private TransferResult transformPotentiallyFulfillingLoop( + private TransferResult updateStoreForPotentiallyFulfillingLoop( TransferResult res, Tree tree) { PotentiallyFulfillingLoop loop = CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); @@ -150,7 +150,7 @@ private TransferResult transformPotentiallyFu public TransferResult visitLessThan( LessThanNode node, TransferInput in) { TransferResult res = super.visitLessThan(node, in); - return transformPotentiallyFulfillingLoop(res, node.getTree()); + return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); } @Override @@ -163,7 +163,7 @@ public TransferResult visitMethodInvocation( ExecutableElement method = node.getTarget().getMethod(); List args = node.getArguments(); res = transferOwnershipForMethodInvocation(method, node, args, res); - res = transformPotentiallyFulfillingLoop(res, node.getTree()); + res = updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); // Check whether the method is annotated @CreatesCollectionObligation. ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 7d978cfbbb14..b67b2fca03a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -164,7 +164,7 @@ public void setRoot(@Nullable CompilationUnitTree newRoot) { } /** - * Replace all the type variables of the given AnnotatedTypeMirror that refer to Collections or + * Replace all the type variables of the given AnnotatedDeclaredType that refer to Collections or * Iterators with Bottom if they are Top. This is because having a Top type parameter is unsafe * and occurs when the type variable is a generic or wildcard without upper bound. We want to * prevent such a Collection/Iterator from holding elements with MustCall obligations. @@ -225,22 +225,22 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar } /** - * Changes the type parameter annotations of collections and iterators to {@code @MustCall} if - * they are currently {@code @MustCallUnknown}. + * Changes the type parameter annotations of resource collections and iterators to + * {@code @MustCall({})} if they are currently {@code @MustCallUnknown}. * *

This is necessary, as the type variable upper bounds for collections is * {@code @MustCallUnknown}. When the type variable is a generic or wildcard with no upper bound, * the type parameter defaults to {@code @MustCallUnknown}, which is both unsound and imprecise. * *

This method changes the type parameter annotations for declared types directly. The other - * overload with access to {@code Element}s handles type parameter annotations for method return - * types and parameters, such that the changes are 'visible' at call-site as well as within the - * method. Changing this on the {@code Tree} is not sufficient. The reason that declared types are - * handled here is that for object initializations where the type parameter is left for inference - * (e.g., {@code new Object<>()}), we don't want to change the type parameter annotation here, but - * wait for the inference instead, which instantiates it with the inferred type and corresponding - * annotation. Access to the {@code Tree} allows us to detect whether we have a new class tree - * without type parameters. + * overload {@link #addComputedTypeAnnotations(Element, AnnotatedTypeMirror)} handles type + * parameter annotations for method return types and parameters, such that the changes are + * 'visible' at call- site as well as within the method. Changing this on the {@code Tree} is not + * sufficient. The reason that declared types are handled here is that for object initializations + * where the type parameter is left for inference (e.g., {@code new Object<>()}), we don't want to + * change the type parameter annotation here, but wait for the inference instead, which + * instantiates it with the inferred type and corresponding annotation. Access to the {@code Tree} + * allows us to detect whether we have a new class tree without type parameters. */ @Override public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { @@ -265,10 +265,8 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool *

This method changes the type parameter annotations of {@code Element}s, which is the * preferred way for method return types and parameters, such that the changes are 'visible' at * call-site as well as within the method. The type parameter annotations at other places is - * handled in the overload of this method with access to the {@code Tree}. The reason is that for - * object initializations where the type parameter is left for inference, we don't want to change - * the type parameter annotation here, but wait for the inference instead, which instantiates it - * with the inferred type and corresponding annotation. {@code new Object<>()} + * handled by {@link #addComputedTypeAnnotations(Tree, AnnotatedTypeMirror, Boolean)}, because it + * has access to the {@code Tree}. */ @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index dff014d11d51..1f3c843641a6 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -364,7 +364,7 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { public Void visitForLoop(ForLoopTree tree, Void p) { boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; if (singleLoopVariable) { - patternMatchFulfillingLoop(tree); + detectCollectionObligationFulfillingLoop(tree); } return super.visitForLoop(tree, p); } @@ -374,7 +374,7 @@ public Void visitForLoop(ForLoopTree tree, Void p) { * * @param tree a `for` loop with exactly one loop variable */ - private void patternMatchFulfillingLoop(ForLoopTree tree) { + private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { List loopBodyStatementList; if (tree.getStatement() instanceof BlockTree) { BlockTree blockT = (BlockTree) tree.getStatement(); @@ -388,13 +388,14 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { if (!(condition instanceof BinaryTree)) { return; } - Name identifierInHeader = verifyAllElementsAreCalledOn(init, (BinaryTree) condition, update); + Name identifierInHeader = + nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); Name iterator = getNameFromStatementTree(init); if (identifierInHeader == null || iterator == null) { return; } ExpressionTree collectionElementTree = - preMatchLoop(loopBodyStatementList, identifierInHeader, iterator); + getLastElementAccessIfLoopValid(loopBodyStatementList, identifierInHeader, iterator); if (collectionElementTree != null) { // Pattern match succeeded, now mark the loop in the respective datastructures. @@ -463,7 +464,7 @@ private void patternMatchFulfillingLoop(ForLoopTree tree) { * @param update the loop update * @return the name of the collection that the loop iterates over all elements of, or null */ - protected Name verifyAllElementsAreCalledOn( + protected Name nameOfCollectionThatAllElementsAreCalledOn( StatementTree init, BinaryTree condition, ExpressionStatementTree update) { Tree.Kind updateKind = update.getExpression().getKind(); if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { @@ -525,7 +526,7 @@ protected Name verifyAllElementsAreCalledOn( * block}. Else, return the last encountered collection access tree consistent with the loop * heaer if it exists and else null. */ - private @Nullable ExpressionTree preMatchLoop( + private @Nullable ExpressionTree getLastElementAccessIfLoopValid( List statements, Name identifierInHeader, Name iterator) { AtomicBoolean blockIsIllegal = new AtomicBoolean(false); final ExpressionTree[] collectionElementTree = {null}; @@ -583,8 +584,8 @@ public Void visitReturn(ReturnTree rt, Void p) { @Override public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { if (isIthCollectionElement(mit, iterator) - && loopHeaderConsistentWithCollection( - identifierInHeader, getNameFromExpressionTree(mit))) { + && identifierInHeader == getNameFromExpressionTree(mit) + && identifierInHeader != null) { collectionElementTree[0] = mit; } return super.visitMethodInvocation(mit, p); @@ -601,10 +602,11 @@ identifierInHeader, getNameFromExpressionTree(mit))) { } /** - * Returns the identifier of an ExpressionTree, or null. + * Returns the simple name of the identifier referenced by the given expression, or {@code null} + * if the expression does not reference an identifier. * * @param expr an expression - * @return the name of the identifier the expression evaluates to or null if it doesn't + * @return the name of the referenced identifier, or {@code null} if none */ protected Name getNameFromExpressionTree(ExpressionTree expr) { if (expr == null) { @@ -628,10 +630,12 @@ protected Name getNameFromExpressionTree(ExpressionTree expr) { } /** - * Returns the name from a {@code StatementTree}, or null. + * Returns the simple name of the identifier declared or referenced by the given statement, or + * {@code null} if the statement does not declare or reference an identifier. * * @param expr the {@code StatementTree} - * @return name of the identifier the expression evaluates to or null if it doesn't + * @return the name of the identifier declared or referenced by the statement, or {@code null} if + * none */ protected Name getNameFromStatementTree(StatementTree expr) { if (expr == null) { @@ -671,25 +675,6 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { } } - /** - * Returns true if the given collection name is consistent with the identifier from the loop - * header. That is, returns true if the names are equal. - * - *

Returns false if any argument is null. - * - * @param idInHeader identifier from loop header - * @param collectionName name of collection - * @return true if the given collection name is consistent with the identifier from the loop - * header - */ - private boolean loopHeaderConsistentWithCollection(Name idInHeader, Name collectionName) { - if (idInHeader == null || collectionName == null) { - return false; - } - boolean namesAreEqual = collectionName == idInHeader; - return namesAreEqual; - } - /** * Returns true if the given tree is of the form collection.get(i), where i is the given index * name. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 63698852c3e2..d2578e8bc09d 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -288,7 +288,7 @@ public Obligation(Set resourceAliases, Set whenTo * @param whenToEnforce when this Obligation should be enforced * @return a new Obligation with the passed traits */ - public Obligation getReplacement( + public abstract Obligation getReplacement( Set resourceAliases, Set whenToEnforce) { return new Obligation(resourceAliases, whenToEnforce); } From 3aeafe79f81737c8346a9cf331b87d3fc1ba1f32 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 8 Aug 2025 18:07:39 +0200 Subject: [PATCH 296/374] fix faulty link --- .../checker/mustcall/MustCallAnnotatedTypeFactory.java | 2 +- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index b67b2fca03a2..920d94de3781 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -265,7 +265,7 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool *

This method changes the type parameter annotations of {@code Element}s, which is the * preferred way for method return types and parameters, such that the changes are 'visible' at * call-site as well as within the method. The type parameter annotations at other places is - * handled by {@link #addComputedTypeAnnotations(Tree, AnnotatedTypeMirror, Boolean)}, because it + * handled by {@link #addComputedTypeAnnotations(Tree, AnnotatedTypeMirror, boolean)}, because it * has access to the {@code Tree}. */ @Override diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index d2578e8bc09d..63698852c3e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -288,7 +288,7 @@ public Obligation(Set resourceAliases, Set whenTo * @param whenToEnforce when this Obligation should be enforced * @return a new Obligation with the passed traits */ - public abstract Obligation getReplacement( + public Obligation getReplacement( Set resourceAliases, Set whenToEnforce) { return new Obligation(resourceAliases, whenToEnforce); } From 02fa7e8ee3d183e3201ed027274ea173e6f4cdcc Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 9 Aug 2025 13:24:15 +0200 Subject: [PATCH 297/374] use old collectors method to prevent runtime error --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 63698852c3e2..21b5ef6dc066 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.Set; import java.util.StringJoiner; +import java.util.stream.Collectors; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -3166,7 +3167,9 @@ public static String formatMissingMustCallMethods(List mustCallVal) { return "method " + formatMustCallMethod(mustCallVal.get(0)); } else { return "methods " - + String.join(", ", mustCallVal.stream().map(s -> formatMustCallMethod(s)).toList()); + + String.join( + ", ", + mustCallVal.stream().map(s -> formatMustCallMethod(s)).collect(Collectors.toList())); } } From c53a15541f0b99e009f4cf4e18a940f701ebfb4f Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 9 Aug 2025 16:36:19 +0200 Subject: [PATCH 298/374] prevent crash in loop body analysis --- .../resourceleak/MustCallConsistencyAnalyzer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 21b5ef6dc066..20d488baaeac 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3385,12 +3385,12 @@ private void patternMatchEnhancedCollectionForLoop( block = predBlocks.iterator().next(); nodeIterator = block.getNodes().iterator(); } else { - throw new BugInCF( - "Encountered more than one CFG Block predeccessor trying to find the" - + " enhanced-for-loop update block. Block: "); - // + block - // + "\nPredecessors: " - // + predBlocks); + // there is no trivial resolution here. Best we can do is just skip this loop, + // which is of course sound. + return; + // throw new BugInCF( + // "Encountered more than one CFG Block predecessor trying to find the" + // + " enhanced-for-loop update block. Block: "); } } node = nodeIterator.next(); From 845006a10fc9bec47e9077099f92ba7b78422398 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sat, 9 Aug 2025 17:18:19 +0200 Subject: [PATCH 299/374] catch BugInCF if tree has unexpected shape when deciding rc --- .../CollectionOwnershipAnnotatedTypeFactory.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 089bf61853a0..84fbfabdefd2 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -342,7 +342,14 @@ public boolean isResourceCollection(Tree tree) { if (tree == null) { return false; } - AnnotatedTypeMirror treeMcType = mcAtf.getAnnotatedType(tree); + AnnotatedTypeMirror treeMcType; + try { + treeMcType = mcAtf.getAnnotatedType(tree); + } catch (BugInCF e) { + // this happens if the tree is not of a supported format, thrown by + // AnnotatedTypeFactory#getAnnotatedType + treeMcType = null; + } List mcValues = getMustCallValuesOfResourceCollectionComponent(treeMcType); return mcValues != null && mcValues.size() > 0; } @@ -359,6 +366,9 @@ public boolean isResourceCollection(Tree tree) { * if there are none or if the given type is not a collection */ public List getMustCallValuesOfResourceCollectionComponent(AnnotatedTypeMirror atm) { + if (atm == null) { + return null; + } boolean isCollectionType = ResourceLeakUtils.isCollection(atm.getUnderlyingType()); AnnotatedTypeMirror componentType = null; From 1b89b55c5d3eea4dfaac22d837fac8fedd9450d9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 10 Aug 2025 18:41:16 +0200 Subject: [PATCH 300/374] prevent crash when rhs of oc field assignment not in store --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 20d488baaeac..59a6d5d23939 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1832,9 +1832,9 @@ private void checkReassignmentToOwningCollectionField( if (TreeUtils.isNullExpression(rhs.getTree())) { // assignment to null allowed return; + } else { + rhsCoType = CollectionOwnershipType.NotOwningCollection; } - throw new BugInCF( - "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); } switch (rhsCoType) { case OwningCollectionWithoutObligation: @@ -1879,8 +1879,7 @@ private void checkReassignmentToOwningCollectionField( if (TreeUtils.isNullExpression(rhs.getTree())) { rhsCoType = CollectionOwnershipType.OwningCollectionBottom; } else { - throw new BugInCF( - "Expression " + rhs + " cannot be found in CollectionOwnership store " + coStore); + rhsCoType = CollectionOwnershipType.NotOwningCollection; } } From adfc3abb74f2a8d1a0b411f1c8a29bc32e4cea25 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Wed, 13 Aug 2025 15:54:14 -0400 Subject: [PATCH 301/374] Fix a typo --- docs/manual/resource-leak-checker.tex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 09bf83055a34..a091cc75672f 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -644,7 +644,8 @@ \sectionAndLabel{Collections of resources}{resource-leak-collections} The Resource Leak Checker handles homogeneous collections of resources. In a homogeneous collection, every element -has exactly the same must-call and called-methods properties. Instances of \texttt{java.util.Iterable} are supported this section calls those ``collections''. Usage of \texttt{java.util.Iterator}s over \texttt{java.util.Iterable}s are also supported, but they are not considered collections. +has exactly the same must-call and called-methods properties. Instances of \texttt{java.util.Iterable} are supported; +this section calls those ``collections''. Usage of \texttt{java.util.Iterator}s over \texttt{java.util.Iterable}s are also supported, but they are not considered collections. What the checker effectively verifies is that any resource collection is properly disposed of. That is, there is some loop that definitely calls the required methods on all elements of the collection. From f9a0665e774c9157411a5308136167f6dd8b0ea3 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Wed, 13 Aug 2025 16:15:58 -0400 Subject: [PATCH 302/374] clarifications and typo fixes in the manual --- docs/manual/resource-leak-checker.tex | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index a091cc75672f..3c2d69b34624 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -652,7 +652,7 @@ Towards that end, the Resource Leak Checker tracks every allocated resource collection in two ways: \begin{itemize} - \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - they can't be used to add or remove elements to and from the collection respectively for example. + \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - for example, they can't be used to add or remove elements to and from the collection, respectively. \item a complementary dataflow analysis tracks an obligation for each allocated resource collection. The obligation can be passed on to method parameters, fields, or return values for example, just like the ownership. The difference is that while the type system tracks at most one owner per resource collection allowed to mutate it without restriction, the dataflow analysis tracks at least one obligation per resource collection to ensure it is definitely fulfilled. \end{itemize} @@ -663,7 +663,7 @@ \item Its type parameter upper bound has non-empty \texttt{MustCall} type. \end{enumerate} -For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collection, but one of type \texttt{Set} is not. +For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collections, but one of type \texttt{Set} is not. \subsectionAndLabel{Ownership type system for resource collections}{resource-leak-collections-ownership-types} Of the two tracking mechanisms described above, the ownership type system is more visible from the user perspective. The type hierarchy is the following: @@ -679,10 +679,10 @@ \end{verbatim} \begin{itemize} - \item \textbf{@OwningCollectionBottom} is the type of non-\textit{resource collections}. It is thus the type the vast majority of expressions have. - \item \textbf{@OwningCollectionWithoutObligation} is the type of \textit{resource collection} references that definitely own the underlying collection, but also definitely don't have any open calling obligations on their elements. For example, a freshly allocated resource collection is of this type. - \item \textbf{@OwningCollection} is the type of resource collection references that definitely own the underlying collection, but might have open calling obligations for their elements. \item \textbf{@NotOwningCollection} is the type of resource collection references that might not own the underlying resource collection. + \item \textbf{@OwningCollection} is the type of resource collection references that definitely own the underlying collection, but might have open calling obligations for their elements. + \item \textbf{@OwningCollectionWithoutObligation} is the type of \textit{resource collection} references that definitely own the underlying collection, but also definitely don't have any open calling obligations on their elements. For example, a freshly allocated resource collection that doesn't yet contain any resources is of this type. + \item \textbf{@OwningCollectionBottom} is the type of non-\textit{resource collections}. It is thus the type the vast majority of expressions have, because most expressions are not collections of resources. \end{itemize} Only \texttt{@OwningCollection} and \texttt{@NotOwningCollection} are permitted as user-written annotations. The other types are only used internally. @@ -721,7 +721,7 @@ This means that by default, a resource collection parameter can only do certain 'safe' operations on the collection that do not add, remove, or overwrite existing elements. But it also means that the parameter does not have any calling obligation for the elements of the collection, and it can accept both owning and not owning collection references due to normal subtyping rules. If an \texttt{OwningCollection} reference is passed to an unannotated parameter, the obligation stays at the call-site. -Annotating the parameter with \texttt{@OwningCollection} overrides this behavior. In this case, the ownership is transferred from the call-site argument to the method parameter. Concretely, this means that the call-site obligation is removed and the parameter takes on the responsibility of disposing of the collection. The type of the argument at call-site is changed to \texttt{@NotOwningCollection} to maintain the invariant that there's at most one owning reference for each resource collection. +Annotating the parameter with \texttt{@OwningCollection} overrides this behavior. In this case, the ownership is transferred from the call-site argument to the method parameter. Concretely, this means that the call-site obligation is removed and the parameter takes on the responsibility of disposing of the collection. The type of the argument at the call-site is changed to \texttt{@NotOwningCollection} to maintain the invariant that there's at most one owning reference for each resource collection. \begin{Verbatim} void m(@OwningCollection Iterable sockets) { @@ -739,7 +739,7 @@ } \end{Verbatim} -Resource collection return types default to \texttt{@OwningCollection}. Returning a resource collection to such a return type fulfills the obligation for the method. The obligation and ownership are both transferred to the call-site return expression. +Resource collection return types default to \texttt{@OwningCollection}. Returning a resource collection from a method with that return type fulfills the collection's obligation. The obligation and ownership are both transferred to the method invocation expression at the call-site. \begin{verbatim} List returnSocketList() { @@ -788,7 +788,7 @@ \begin{itemize} \item It checks that the loop does not terminate early. - \item It leverages the \texttt{CalledMethods} analysis to determine the methods definitely called on the loop iterator variable. This means that anything that changes the \texttt{CalledMethods} type of the iterator variable is supported, for example passing it as an argument to a method annotated \texttt{@EnsuresCalledMethods(``m'')}, or using try-with constructs. Nullness checks are also supported. + \item It leverages the \texttt{CalledMethods} analysis to determine the methods definitely called on the loop iterator variable. This means that anything that changes the \texttt{CalledMethods} type of the iterator variable is supported, for example passing it as an argument to a method annotated \texttt{@EnsuresCalledMethods(``m'')}, or using try-with constructs. Nullness checks are also supported: that is, it is sufficient to fulfill the obligation for every non-null element of the collection; note that this is a simple, syntactic check, not a full-fledged nullness analysis, and so it might fail to certify loops that use an unorthodox method for nullness checking. \end{itemize} If an indexed \texttt{for} loop is used, the checker must additionally check that the loop does in fact iterate over all elements of the collection, and that the loop doesn't write to the loop variable for example. The checks are the following: From 73d6163fc4655d20009fb5e2b5219fbddea8c207 Mon Sep 17 00:00:00 2001 From: Martin Kellogg Date: Wed, 13 Aug 2025 16:26:35 -0400 Subject: [PATCH 303/374] explanatory comment --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 84fbfabdefd2..7dc5ffc82826 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -216,6 +216,10 @@ public CollectionOwnershipStore getStoreForBlock( : flowResult.getStoreBefore(succBlock); } + // Note that this method is overridden here because the collections ownership + // typechecker runs last in the RLC, not because this has anything to do with + // collections. Whatever checker runs last in the RLC must do this. TODO: make this + // run last in a more sensible way. @Override public void postAnalyze(ControlFlowGraph cfg) { ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); From 45042949ef8b75c96b0d944db9ed57fe7292b416 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:27:23 +0200 Subject: [PATCH 304/374] clarify comment --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 7dc5ffc82826..0b2c927e49f1 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -245,8 +245,8 @@ public void postAnalyze(ControlFlowGraph cfg) { * Returns true if the given type is a resource collection: a type assignable from {@code * Collection} whose single type var has non-empty MustCall type. * - *

This overload should be used before computation of AnnotatedTypeMirrors is completed, in - * particular in addComputedTypeAnnotations(AnnotatedTypeMirror). + *

This overload should be used only before computation of AnnotatedTypeMirrors is completed, + * in particular in addComputedTypeAnnotations(AnnotatedTypeMirror). * * @param t the AnnotatedTypeMirror * @return true if t is a resource collection From 912aa9a71ec9bfa2f411feba50277e6bd1e2268e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:27:29 +0200 Subject: [PATCH 305/374] remove commented out code --- .../CollectionOwnershipTransfer.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 1fe5ed59adce..cff2f91a6e3d 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -91,24 +91,6 @@ public TransferResult visitAssignment( default: } } - // boolean assignmentOfOwningCollectionArrayElement = - // lhsIsOwningCollection && lhs.getTree().getKind() == Tree.Kind.ARRAY_ACCESS; - - // if (assignmentOfOwningCollectionArrayElement) { - // ExpressionTree arrayExpression = ((ArrayAccessTree) lhs.getTree()).getExpression(); - // JavaExpression arrayJE = JavaExpression.fromTree(arrayExpression); - - // boolean inAssigningLoop = - // MustCallOnElementsAnnotatedTypeFactory.doesAssignmentCreateArrayObligation( - // (AssignmentTree) node.getTree()); - - // // transformation of assigning loop is handled at the loop condition node, - // // not the assignment node. So, only transform if not in an assigning loop. - // if (!inAssigningLoop) { - // store = - // transformWriteToOwningCollection(arrayJE, arrayExpression, node.getExpression(), - // store); - // } return res; } From 639ff0ddbc133be3919fcfa75c39de7a39732519 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:27:43 +0200 Subject: [PATCH 306/374] add explanation why loop matching is in mc-visitor --- .../checkerframework/checker/mustcall/MustCallVisitor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 1f3c843641a6..b7bac877000d 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -353,8 +353,11 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { } // /////////////////////////////////////////////////////////////////////////// - // syntactically match for-loops that iterate over all elements of a collection on the AST - // + // Syntactically match for-loops that iterate over all elements of a collection on the AST. + // This happens here in the MustCallVisitor instead of the CollectionOwnershipVisitor, which + // would be the natural place to put this logic, because the matching must be completed + // before the CollectionOwnershipTransfer logic runs, and the CollectionOwnershipVisitor runs + // after the CollectionOwnershipTransfer. /** * Records, in the {@code @CollectionOwnershipAnnotatedTypeFactory}, loops that call a method on From 26a43c197465d4535d678a288609a65233934216 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:28:02 +0200 Subject: [PATCH 307/374] update explanation for expected test behavior --- checker/tests/resourceleak/SocketIntoList.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index a35c397e7878..190d89db7a11 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -16,8 +16,7 @@ public void test1(List<@MustCall({}) Socket> l) { public List test2(@OwningCollection List l) { // s is unconnected, so no error is expected when it's stored into the list. - // But, if the list is unannotated, we do get an error at its declaration site - // (as expected, due to #5912). + // However, l would be allowed to store even connected sockets. Socket s = new Socket(); l.add(s); return l; From 4b2a22309c5512710e8d4b025704bdce7051e9c8 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:28:17 +0200 Subject: [PATCH 308/374] add a test case --- .../CollectionOwnershipBasicTyping.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index 8a60286f845b..ee507b3b338a 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -49,12 +49,22 @@ Collection checkIllegalNotOwningReturn() { // this is the correct version of above. It passes ownership to another method first, // and then returns the non-owning reference. @NotOwningCollection - Collection checkLegalNotOwningReturn() { + Collection checkLegalNotOwningReturn1() { List list = new ArrayList<>(); + try { + list.add(new Socket("", 42)); + } catch (Exception e) { + } closeElements(list); return list; } + @NotOwningCollection + Collection checkLegalNotOwningReturn2() { + List list = new ArrayList<>(); + return list; + } + // check that unrefinement in assignments is allowed. void checkUnrefinement() { List list = new ArrayList<>(); From 4bf1f9208c9f168a05c14170d45a8fc047c787f1 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:28:29 +0200 Subject: [PATCH 309/374] remove commented out defaults for OCBottom --- .../qual/OwningCollectionBottom.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java index 40c2cbcbf345..dcfe1b75c4d8 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionBottom.java @@ -22,19 +22,7 @@ @SubtypeOf({OwningCollectionWithoutObligation.class}) @DefaultQualifierInHierarchy @DefaultFor({ - // TypeUseLocation.ALL, - // TypeUseLocation.CONSTRUCTOR_RESULT, TypeUseLocation.EXCEPTION_PARAMETER, - // TypeUseLocation.EXPLICIT_LOWER_BOUND, - // TypeUseLocation.EXPLICIT_UPPER_BOUND, - // TypeUseLocation.FIELD, - // TypeUseLocation.IMPLICIT_LOWER_BOUND, - // TypeUseLocation.IMPLICIT_UPPER_BOUND, - // TypeUseLocation.LOCAL_VARIABLE, - // TypeUseLocation.LOWER_BOUND, - // TypeUseLocation.OTHERWISE, - // TypeUseLocation.PARAMETER, - // TypeUseLocation.RECEIVER, TypeUseLocation.RESOURCE_VARIABLE, TypeUseLocation.RETURN, TypeUseLocation.UPPER_BOUND From 65abe44c1f6a085aa4b3bef89873f7e18c15ad1e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:29:06 +0200 Subject: [PATCH 310/374] move field example code before explanation in manual --- docs/manual/resource-leak-checker.tex | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 3c2d69b34624..90210c0c4770 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -817,17 +817,6 @@ \subsectionAndLabel{Resource collection fields}{resource-leak-collections-fields} By default, resource collection fields are owned by the enclosing class. Static fields are not supported and an error is reported for a declaration of a static resource collection field. - -To verify a resource collection field \textit{field}, the checker looks for the following: -\begin{enumerate} - \item A \texttt{@MustCall} annotation on the enclosing class. In this case, the class is of type \texttt{@MustCall(``close'')} by subclassing Closeable. - \item Among the methods in the \texttt{@MustCall} type of the enclosing class (usually there's just one), the checker now looks for the post-condition annotation \texttt{@CollectionFieldDestructor(``field'')}. - \item The annotation \texttt{@CollectionFieldDestructor(``field'')} asserts that at the method exit, \textit{field} has type \texttt{@OwningCollectionWithoutObligation}, which the checker verifies. - \item Methods that call 'unsafe' methods creating obligations for the resource collection field, such as \texttt{socketList.add(Socket)} in this case, must have a \texttt{@CreatesMustCallFor(``this'')} annotation. -\end{enumerate} - -This now shifts the burden to the client of the \texttt{Aggregator} class to call \texttt{close()} on instances before they leave scope. - Consider the following example: \begin{verbatim} @@ -864,6 +853,16 @@ } \end{verbatim} +To verify a resource collection field \textit{field}, the checker looks for the following: +\begin{enumerate} + \item A \texttt{@MustCall} annotation on the enclosing class. In this case, the class is of type \texttt{@MustCall(``close'')} by subclassing Closeable. + \item Among the methods in the \texttt{@MustCall} type of the enclosing class (usually there's just one), the checker now looks for the post-condition annotation \texttt{@CollectionFieldDestructor(``field'')}. + \item The annotation \texttt{@CollectionFieldDestructor(``field'')} asserts that at the method exit, \textit{field} has type \texttt{@OwningCollectionWithoutObligation}, which the checker verifies. + \item Methods that call 'unsafe' methods creating obligations for the resource collection field, such as \texttt{socketList.add(Socket)} in this case, must have a \texttt{@CreatesMustCallFor(``this'')} annotation. +\end{enumerate} + +This now shifts the burden to the client of the \texttt{Aggregator} class to call \texttt{close()} on instances before they leave scope. + If a resource collection field is final, the checker doesn't have to verify assignments to it. If it is not final, the check verifies assignments in the following way: \begin{itemize} \item Assignments in initializer blocks are not permitted. From 51792060de6e5cdabeaa2fe9185f0531ecd51a2b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 17 Aug 2025 21:46:47 +0200 Subject: [PATCH 311/374] comment out mentions of iterator support in rl manual --- docs/manual/resource-leak-checker.tex | 139 +++++++++++++------------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 90210c0c4770..0dc1112ca381 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -645,7 +645,8 @@ The Resource Leak Checker handles homogeneous collections of resources. In a homogeneous collection, every element has exactly the same must-call and called-methods properties. Instances of \texttt{java.util.Iterable} are supported; -this section calls those ``collections''. Usage of \texttt{java.util.Iterator}s over \texttt{java.util.Iterable}s are also supported, but they are not considered collections. +this section calls those ``collections''. +% Usage of \texttt{java.util.Iterator}s over \texttt{java.util.Iterable}s are also supported, but they are not considered collections. What the checker effectively verifies is that any resource collection is properly disposed of. That is, there is some loop that definitely calls the required methods on all elements of the collection. @@ -659,7 +660,7 @@ For the purposes of this checker, \textit{resource collections} are precisely defined, as the objects of interest for both the type system and obligation tracking. A Java expression is a \textit{resource collection} if it is: \begin{enumerate} - \item A \texttt{java.util.Iterable} or implementation thereof (which includes the entire Java Collections framework), or a \texttt{java.util.Iterator}, and: + \item A \texttt{java.util.Iterable} or implementation thereof (which includes the entire Java Collections framework), and:%, or a \texttt{java.util.Iterator} \item Its type parameter upper bound has non-empty \texttt{MustCall} type. \end{enumerate} @@ -886,73 +887,73 @@ \item The enclosing class does not have to fulfill any obligations on the field. \end{itemize} -\subsectionAndLabel{Iterators over resource collections}{resource-leak-collections-iterators} -Iterators are frequently used to traverse collections. It is always safe to create an iterator for any non-resource collection. Since an iterator takes a snapshot of the collection at the time it is created, it does not matter what happens with the collection after the iterator is created (else, a \ is thrown for the next usage of the iterator). -An iterator created for a resource collection has the same collection ownership type as the resource collection. - -\begin{verbatim} -// `socketList` is @OwningCollectionWithoutObligation -Iterator socketIterator = socketList.iterator(); -// `socketIterator` is @OwningCollectionWithoutObligation -\end{verbatim} - -The only concering operation an iterator can do is \texttt{remove()}, as the removed element might have open calling obligations. - -Just like a \texttt{java.util.Collection} of type \texttt{@NotOwningCollection}, an iterator of this type is not allowed to call \texttt{remove()} at all. Iterators of type \texttt{@OwningCollectionBottom} and \texttt{@OwningCollectionWithoutObligation} can always call \texttt{remove()}. The interesting remaining case is that of an \texttt{@OwningCollection} iterator. -Such an iterator and the values its calls to \texttt{next()} return are tracked with special obligations. This is to ensure that whenever an \texttt{@OwningCollection} iterator calls \texttt{close()}, the value returned by the previous call to \texttt{Iterator.next()} has its \texttt{MustCall} obligations fulfilled. - -Here is an example of unsafe iterator usage: - -\begin{verbatim} -List foo(@OwningCollection List list) { - Iterator iter = list.iterator(); - // `iter` is @OwningCollection and must be tracked - iter.next(); - iter.remove(); // unsafe! - return list; -} -\end{verbatim} - -\begin{verbatim} - error: [required.method.not.called] @MustCall method close may not have been invoked on iter.next() or any of its aliases. - iter.next(); - ^ -\end{verbatim} - -The removed element must have its must-call obligations fulfilled. Here is an example of safe usage: - -\begin{verbatim} -List foo(@OwningCollection List list) { - Iterator iter = list.iterator(); - // `iter` is @OwningCollection and must be tracked - try { - iter.next().close(); - } catch (Exception e) { - } - iter.remove(); - return list; -} -\end{verbatim} - -The returned element may also be stored in a variable first and the fulfillment of the obligation of the returned element may also occur after the call to \texttt{remove()}: - -\begin{verbatim} -List foo(@OwningCollection List list) { - Iterator iter = list.iterator(); - // `iter` is @OwningCollection and must be tracked - Socket s = iter.next(); - iter.remove(); - try { - s.close(); - } catch (Exception e) { - } - return list; -} -\end{verbatim} - -The returned element by \texttt{iter.next()} may have its must-call obligation fulfilled in any way described by the resource leak checker, even by transfering its ownership (for example by storing it in a field or passing it as an \texttt{@OwningCollection} method argument). - -Iterators over collections with obligations may also be returned, passed as method or constructor arguments and even be stored in fields. +% \subsectionAndLabel{Iterators over resource collections}{resource-leak-collections-iterators} +% Iterators are frequently used to traverse collections. It is always safe to create an iterator for any non-resource collection. Since an iterator takes a snapshot of the collection at the time it is created, it does not matter what happens with the collection after the iterator is created (else, a \ is thrown for the next usage of the iterator). +% An iterator created for a resource collection has the same collection ownership type as the resource collection. + +% \begin{verbatim} +% // `socketList` is @OwningCollectionWithoutObligation +% Iterator socketIterator = socketList.iterator(); +% // `socketIterator` is @OwningCollectionWithoutObligation +% \end{verbatim} + +% The only concering operation an iterator can do is \texttt{remove()}, as the removed element might have open calling obligations. + +% Just like a \texttt{java.util.Collection} of type \texttt{@NotOwningCollection}, an iterator of this type is not allowed to call \texttt{remove()} at all. Iterators of type \texttt{@OwningCollectionBottom} and \texttt{@OwningCollectionWithoutObligation} can always call \texttt{remove()}. The interesting remaining case is that of an \texttt{@OwningCollection} iterator. +% Such an iterator and the values its calls to \texttt{next()} return are tracked with special obligations. This is to ensure that whenever an \texttt{@OwningCollection} iterator calls \texttt{close()}, the value returned by the previous call to \texttt{Iterator.next()} has its \texttt{MustCall} obligations fulfilled. + +% Here is an example of unsafe iterator usage: + +% \begin{verbatim} +% List foo(@OwningCollection List list) { +% Iterator iter = list.iterator(); +% // `iter` is @OwningCollection and must be tracked +% iter.next(); +% iter.remove(); // unsafe! +% return list; +% } +% \end{verbatim} + +% \begin{verbatim} +% error: [required.method.not.called] @MustCall method close may not have been invoked on iter.next() or any of its aliases. +% iter.next(); +% ^ +% \end{verbatim} + +% The removed element must have its must-call obligations fulfilled. Here is an example of safe usage: + +% \begin{verbatim} +% List foo(@OwningCollection List list) { +% Iterator iter = list.iterator(); +% // `iter` is @OwningCollection and must be tracked +% try { +% iter.next().close(); +% } catch (Exception e) { +% } +% iter.remove(); +% return list; +% } +% \end{verbatim} + +% The returned element may also be stored in a variable first and the fulfillment of the obligation of the returned element may also occur after the call to \texttt{remove()}: + +% \begin{verbatim} +% List foo(@OwningCollection List list) { +% Iterator iter = list.iterator(); +% // `iter` is @OwningCollection and must be tracked +% Socket s = iter.next(); +% iter.remove(); +% try { +% s.close(); +% } catch (Exception e) { +% } +% return list; +% } +% \end{verbatim} + +% The returned element by \texttt{iter.next()} may have its must-call obligation fulfilled in any way described by the resource leak checker, even by transfering its ownership (for example by storing it in a field or passing it as an \texttt{@OwningCollection} method argument). + +% Iterators over collections with obligations may also be returned, passed as method or constructor arguments and even be stored in fields. \subsectionAndLabel{JDK Method Signatures}{resource-leak-collections-jdk-methods} [[TODO: Add a table with a number of collection/iterable methods and their collection ownership annotations]] From b4fe2ef832a83853645299f321f879abe0508d4a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 26 Aug 2025 14:11:06 +0200 Subject: [PATCH 312/374] parser moved --- .../CollectionOwnershipAnnotatedTypeFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 0b2c927e49f1..809923241460 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -43,6 +43,7 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.JavaExpressionParseException; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -52,7 +53,6 @@ import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.framework.util.JavaExpressionParseUtil; import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; @@ -535,7 +535,7 @@ public boolean expressionIsFieldAccess(String e, VariableElement field) { try { JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); - } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + } catch (JavaExpressionParseException ex) { // The parsing error will be reported elsewhere, assuming e was derived from an // annotation. return false; @@ -556,7 +556,7 @@ public JavaExpression stringToJavaExpression(String s, ExecutableElement method) if (methodTree instanceof MethodTree) { try { return StringToJavaExpression.atMethodBody(s, (MethodTree) methodTree, checker); - } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + } catch (JavaExpressionParseException ex) { return null; } } From fcfc6844c32261d8f61221d1cc0d24a26335204e Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 26 Aug 2025 16:59:05 +0200 Subject: [PATCH 313/374] add obligation upon CreatsCollectionObligation even for @OC --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index a04d56d6c673..45e5913c5321 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -827,6 +827,8 @@ private void addObligationsForCreatesCollectionObligationAnno( } switch (receiverType) { case OwningCollectionWithoutObligation: + // fall through + case OwningCollection: if (!receiverIsOwningField) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); @@ -835,8 +837,6 @@ private void addObligationsForCreatesCollectionObligationAnno( CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); } } - // fall through - case OwningCollection: if (receiverIsOwningField) { TreePath currentPath = cmAtf.getPath(node.getTree()); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); From 0de2d9cc63525532d3038871f1eb248445aa0cea Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 8 Sep 2025 18:08:45 -0700 Subject: [PATCH 314/374] Fix bug in CFG construction for preincrement --- .../resourceleak/DequePushIncrement.java | 12 +++++++++++ .../cfg/builder/CFGTranslationPhaseOne.java | 21 +++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 checker/tests/resourceleak/DequePushIncrement.java diff --git a/checker/tests/resourceleak/DequePushIncrement.java b/checker/tests/resourceleak/DequePushIncrement.java new file mode 100644 index 000000000000..3cebc9403474 --- /dev/null +++ b/checker/tests/resourceleak/DequePushIncrement.java @@ -0,0 +1,12 @@ +import java.util.ArrayDeque; +import java.util.Deque; + +public class DequePushIncrement { + + Deque counters = new ArrayDeque(); + + public void visitNewClass() { + int i = 0; + counters.push(++i); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index aad674fbbd78..1727df5f42d0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -4159,7 +4159,7 @@ public Node visitInstanceOf(InstanceOfTree tree, Void p) { @Override public Node visitUnary(UnaryTree tree, Void p) { - Node result; + Node result = null; Tree.Kind kind = tree.getKind(); switch (kind) { case BITWISE_COMPLEMENT: @@ -4186,7 +4186,7 @@ public Node visitUnary(UnaryTree tree, Void p) { throw new BugInCF("Unexpected unary tree kind: " + kind); } extendWithNode(result); - return result; + break; } case LOGICAL_COMPLEMENT: @@ -4195,7 +4195,7 @@ public Node visitUnary(UnaryTree tree, Void p) { Node expr = scan(tree.getExpression(), p); result = new ConditionalNotNode(tree, unbox(expr)); extendWithNode(result); - return result; + break; } case POSTFIX_DECREMENT: @@ -4239,12 +4239,13 @@ public Node visitUnary(UnaryTree tree, Void p) { result = new LocalVariableNode(resultExpr); result.setInSource(false); extendWithNode(result); - createIncrementOrDecrementAssign(tree, expr, isIncrement, isPostfix); - } else { - result = createIncrementOrDecrementAssign(tree, expr, isIncrement, isPostfix); - extendWithNode(result); } - return result; + AssignmentNode unaryAssign = + createIncrementOrDecrementAssign(tree, expr, isIncrement, isPostfix); + if (!isPostfix) { + result = unaryAssign; + } + break; } case OTHER: @@ -4254,11 +4255,13 @@ public Node visitUnary(UnaryTree tree, Void p) { Node expr = scan(tree.getExpression(), p); result = new NullChkNode(tree, expr); extendWithNode(result); - return result; + break; } throw new BugInCF("Unknown kind (" + kind + ") of unary expression: " + tree); } + + return result; } /** From 9d61bff60bcc9e9396b7bb49bcd1ee279e452459 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 8 Sep 2025 18:18:43 -0700 Subject: [PATCH 315/374] Fixup --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 1727df5f42d0..87ad0230f7b6 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -4245,7 +4245,7 @@ public Node visitUnary(UnaryTree tree, Void p) { if (!isPostfix) { result = unaryAssign; } - break; + return result; } case OTHER: From 11ee250bf49b1ddc303689e45b6490cdadc6acff Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 8 Sep 2025 18:20:30 -0700 Subject: [PATCH 316/374] Return locally --- .../dataflow/cfg/builder/CFGTranslationPhaseOne.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 87ad0230f7b6..cd6b23943ea3 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -4159,7 +4159,7 @@ public Node visitInstanceOf(InstanceOfTree tree, Void p) { @Override public Node visitUnary(UnaryTree tree, Void p) { - Node result = null; + Node result; Tree.Kind kind = tree.getKind(); switch (kind) { case BITWISE_COMPLEMENT: @@ -4186,7 +4186,7 @@ public Node visitUnary(UnaryTree tree, Void p) { throw new BugInCF("Unexpected unary tree kind: " + kind); } extendWithNode(result); - break; + return result; } case LOGICAL_COMPLEMENT: @@ -4195,7 +4195,7 @@ public Node visitUnary(UnaryTree tree, Void p) { Node expr = scan(tree.getExpression(), p); result = new ConditionalNotNode(tree, unbox(expr)); extendWithNode(result); - break; + return result; } case POSTFIX_DECREMENT: @@ -4211,6 +4211,7 @@ public Node visitUnary(UnaryTree tree, Void p) { boolean isPostfix = kind == Tree.Kind.POSTFIX_INCREMENT || kind == Tree.Kind.POSTFIX_DECREMENT; + result = null; // for definite assignment; it is assigned in isPostfix and in !isPostfix if (isPostfix) { TypeMirror exprType = TreeUtils.typeOf(exprTree); VariableTree tempVarDecl = @@ -4255,13 +4256,11 @@ public Node visitUnary(UnaryTree tree, Void p) { Node expr = scan(tree.getExpression(), p); result = new NullChkNode(tree, expr); extendWithNode(result); - break; + return result; } throw new BugInCF("Unknown kind (" + kind + ") of unary expression: " + tree); } - - return result; } /** From 23da55c57618e36e16a11f5eebbccdf70a831a0a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 13:50:24 +0200 Subject: [PATCH 317/374] fix spelling mistake in manual --- docs/manual/resource-leak-checker.tex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 8d5174589426..51a8d5edefc0 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -870,7 +870,7 @@ \item Thanks to the above two restrictions, it can be assumed that an owned resource collection field is \texttt{@OwningCollectionWithoutObligation} at the beginning of a constructor. An assignment to the field is now allowed if the field is \texttt{@OwningCollectionWithoutObligation} just before the assignment. This condition holds in any other (non-constructor) method too, but the field is \texttt{@OwningCollection} at the method entrance, unlike for a constructor. A legal assignment to an owning resource collection field resolves the obligation of the right-hand side. \end{itemize} -Resource collection fields owned by the enclosing class are special cases for ownership transfer, as the checker doesn't allow transfering the ownership away from such a field. If for example a member method were able to transfer the ownership of the field to an external method parameter, the checker would have to track that the field is no longer the owner and set its type to \texttt{@NotOwningCollection} for future method invocations on the class instance. For the sake of simplicity, the checker doesn't track this and thus doesn't allow ownership transfers for fields. In particular: +Resource collection fields owned by the enclosing class are special cases for ownership transfer, as the checker doesn't allow transferring the ownership away from such a field. If for example a member method were able to transfer the ownership of the field to an external method parameter, the checker would have to track that the field is no longer the owner and set its type to \texttt{@NotOwningCollection} for future method invocations on the class instance. For the sake of simplicity, the checker doesn't track this and thus doesn't allow ownership transfers for fields. In particular: \begin{itemize} \item The field cannot be passed as an argument to an \texttt{@OwningCollection} parameter. @@ -950,7 +950,7 @@ % } % \end{verbatim} -% The returned element by \texttt{iter.next()} may have its must-call obligation fulfilled in any way described by the resource leak checker, even by transfering its ownership (for example by storing it in a field or passing it as an \texttt{@OwningCollection} method argument). +% The returned element by \texttt{iter.next()} may have its must-call obligation fulfilled in any way described by the resource leak checker, even by transferring its ownership (for example by storing it in a field or passing it as an \texttt{@OwningCollection} method argument). % Iterators over collections with obligations may also be returned, passed as method or constructor arguments and even be stored in fields. From 8b35b866ddef25c87878224766d89eb49cf7fb63 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 13:50:50 +0200 Subject: [PATCH 318/374] add explanation when catching InvalidLoopBodyAnalysis Exception --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 45e5913c5321..6b373a67ac72 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3539,6 +3539,7 @@ public void analyzeObligationFulfillingLoop( visited, worklist); } catch (InvalidLoopBodyAnalysisException e) { + // Expected when analyzing unreachable loop bodies. Safely abort the analysis. return; } } From e40bbbfa89cdcbcfdce182d14624a4668868dc8b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:00:49 +0200 Subject: [PATCH 319/374] mark method as @override --- .../resourceleak-collections/OwningCollectionFieldTyping.java | 1 + 1 file changed, 1 insertion(+) diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 650d7fe314b1..6c39fd8eea20 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -188,6 +188,7 @@ public OwningFinalField(@OwningCollection List list) { } @CollectionFieldDestructor("fieldList") + @Override public void close() { for (Resource r : fieldList) { r.close(); From a552a5670d774a146436a7ea9f8e5d20e614e076 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:01:01 +0200 Subject: [PATCH 320/374] prefer this.equals over other.equals --- .../dataflow/expression/IteratedCollectionElement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index 4376fbb98e1b..a2f0ab2bad8d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -38,7 +38,7 @@ public boolean equals(@Nullable Object obj) { return false; } IteratedCollectionElement other = (IteratedCollectionElement) obj; - return other.tree.equals(this.tree) && other.node.equals(this.node); + return this.tree.equals(other.tree) && this.node.equals(other.node); } @Override From 04284adc83a6642a3c8de302f97ba4debb13b00a Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:01:12 +0200 Subject: [PATCH 321/374] remove array example from rlc4c manual --- docs/manual/resource-leak-checker.tex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 51a8d5edefc0..07e94fbb8f7c 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -663,7 +663,7 @@ \item Its type parameter upper bound has non-empty \texttt{MustCall} type. \end{enumerate} -For example, expressions of type \texttt{Socket[]} or \texttt{Iterable} are both resource collections, but one of type \texttt{Set} is not. +For example, expressions of type \texttt{List} or \texttt{Iterable} are both resource collections, but one of type \texttt{Set} is not. \subsectionAndLabel{Ownership type system for resource collections}{resource-leak-collections-ownership-types} Of the two tracking mechanisms described above, the ownership type system is more visible from the user perspective. The type hierarchy is the following: @@ -820,7 +820,7 @@ Consider the following example: \begin{verbatim} -class Aggregator extends Closeable { +class Aggregator implements Closeable { List socketList = new ArrayList<>(); public Aggregator() {} From af26a6323296f9b46fe884abec58575941affad9 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:06:51 +0200 Subject: [PATCH 322/374] fix typo in comment --- .../resourceleak-collections/CollectionOwnershipDefaults.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java index 9aa11d255414..7c5892fe7a08 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java @@ -17,7 +17,7 @@ class CollectionOwnershipDefaults { /* * Check that manual MustCall annotations are correctly considered when deciding whether - * something is a resoure collection. + * something is a resource collection. */ public void detectResourceCollection( List<@MustCall({}) Socket> l1, From 54b60006359be894ed59ed55fcb5f06e0cd89586 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:07:11 +0200 Subject: [PATCH 323/374] add override decorators to test file --- .../resourceleak-collections/OwningCollectionFieldTyping.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 6c39fd8eea20..6629b0f18094 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -143,6 +143,7 @@ List getList() { } @CollectionFieldDestructor("fieldList") + @Override public void close() { for (Resource r : fieldList) { r.close(); @@ -160,6 +161,7 @@ List getList() { } @CollectionFieldDestructor("fieldList") + @Override public void close() { for (Resource r : fieldList) { r.close(); @@ -172,6 +174,7 @@ class OwningFieldWithNullInitializer implements Closeable { List fieldList = null; @CollectionFieldDestructor("fieldList") + @Override public void close() { for (Resource r : fieldList) { r.close(); From 69175cf1668eb84a6020edc0073a72ac38b5892c Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:07:25 +0200 Subject: [PATCH 324/374] explicitly use MustCall({}) instead of MustCall --- .../CollectionOwnershipBasicTyping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java index ee507b3b338a..35aa0679afdf 100644 --- a/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java +++ b/checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java @@ -105,7 +105,7 @@ void testDiamond() { void checkAdvancedDiamond() { // check that this doesn't create an obligation and defaults to @OwningCollectionBottom - Collection<@MustCall Socket> c = new ArrayList<>(); + Collection<@MustCall({}) Socket> c = new ArrayList<>(); checkArgIsBottom(c); // these all create obligations From f9454916b51d4e4bc6e6cb85338d03c9e5316f85 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 14:45:22 +0200 Subject: [PATCH 325/374] remove exceptional stores noop --- .../checker/rlccalledmethods/RLCCalledMethodsTransfer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index c50742e0041b..eb6f6cc32505 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -123,8 +123,6 @@ private void updateStoreForIteratedCollectionElement( s.replaceValue( collectionElement, analysis.createSingleAnnotationValue(newAnno, collectionElement.getType()))); - // TODO unsure this is necessary/harmful - result.withExceptionalStores(exceptionalStores); } } From 527004c9082855d9c1d4effbc3f5a94f9db83098 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 16:55:11 +0200 Subject: [PATCH 326/374] annotated @CreatesCollectionObligation as documented --- .../collectionownership/qual/CreatesCollectionObligation.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java index 203db61aa4d8..9c5dfaaf88e4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -21,6 +22,7 @@ * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ +@Documented @InheritedAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) From 3b3a8df87f21ada48b79868cd32dbf6b0c9bf848 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Tue, 7 Oct 2025 16:56:17 +0200 Subject: [PATCH 327/374] fix doc for createscollectionobligation --- .../collectionownership/qual/CreatesCollectionObligation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java index 9c5dfaaf88e4..281dff45b807 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -16,7 +16,7 @@ * and a CollectionObligation is created for each {@code @MustCall} method of the type variable of * the receiver. * - *

This annotation should only be used on method and constructor declarations of collections, as + *

This annotation should only be used on method declarations of collections, as * defined by the CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code * java.util.Iterator} implementations. * From b105a8abcc935ef84a4592e6e8f9a4bbef95d909 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 8 Oct 2025 10:39:21 +0200 Subject: [PATCH 328/374] improve javadoc --- .../qual/CreatesCollectionObligation.java | 6 +++--- .../collectionownership/qual/NotOwningCollection.java | 2 ++ .../checker/collectionownership/qual/OwningCollection.java | 2 ++ .../qual/OwningCollectionWithoutObligation.java | 2 ++ .../collectionownership/qual/PolyOwningCollection.java | 1 + .../checker/calledmethods/CalledMethodsTransfer.java | 1 + 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java index 281dff45b807..ad68ff3246c2 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -16,9 +16,9 @@ * and a CollectionObligation is created for each {@code @MustCall} method of the type variable of * the receiver. * - *

This annotation should only be used on method declarations of collections, as - * defined by the CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code - * java.util.Iterator} implementations. + *

This annotation should only be used on method declarations of collections, as defined by the + * CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code java.util.Iterator} + * implementations. * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java index 297bbe4b28cd..3ac85e3d0a8d 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,6 +14,7 @@ * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java index 00cdfda0983d..721ebdeef6c4 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -16,6 +17,7 @@ * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({NotOwningCollection.class}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java index f9842c805d73..13c0f8b4d469 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership.qual; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -18,6 +19,7 @@ * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({OwningCollection.class}) diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java index 696568b4dee9..a0d64255ebf1 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java @@ -12,6 +12,7 @@ * * @checker_framework.manual #resource-leak-checker Resource Leak Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + * @see NotOwningCollection */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java index f1f5eb7ce35b..01178e62c53d 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java @@ -43,6 +43,7 @@ public class CalledMethodsTransfer extends AccumulationTransfer { * The element for the CalledMethods annotation's value element. Stored in a field in this class * to prevent the need to cast to CalledMethods ATF every time it's used. */ + // Protected for use by the subclass RLCCalledMethodsTransfer (which is in a different package) protected final ExecutableElement calledMethodsValueElement; /** The type mirror for {@link Exception}. */ From a8111ef7c345d632f4b8fe2bd0de5f01add8b0ef Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 8 Oct 2025 11:22:01 +0200 Subject: [PATCH 329/374] address trivial coderabbit PR comments --- .../collectionownership/CollectionOwnershipChecker.java | 4 +++- .../checker/rlccalledmethods/messages.properties | 4 ++-- .../checker/test/junit/ResourceLeakCollections.java | 2 +- checker/tests/resourceleak/EnhancedFor.java | 6 +++--- checker/tests/resourceleak/Issue4815.java | 2 +- checker/tests/resourceleak/SocketIntoList.java | 7 +++---- .../dataflow/cfg/visualize/CFGVisualizer.java | 4 ++-- .../dataflow/expression/IteratedCollectionElement.java | 8 ++++---- .../checkerframework/common/basetype/BaseTypeVisitor.java | 6 +++--- .../checkerframework/framework/flow/CFAbstractStore.java | 4 +++- .../java/org/checkerframework/javacutil/TreeUtils.java | 5 +++-- 11 files changed, 28 insertions(+), 24 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java index 217d62991802..99480c804579 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java @@ -1,5 +1,6 @@ package org.checkerframework.checker.collectionownership; +import java.util.LinkedHashSet; import java.util.Set; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsChecker; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -17,7 +18,8 @@ public CollectionOwnershipChecker() {} @Override protected Set> getImmediateSubcheckerClasses() { - Set> checkers = super.getImmediateSubcheckerClasses(); + Set> checkers = + new LinkedHashSet<>(super.getImmediateSubcheckerClasses()); checkers.add(RLCCalledMethodsChecker.class); return checkers; } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties index 1935440e2db2..412de3c7dcab 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties @@ -1,2 +1,2 @@ -owning.override.return=Incompatible ownership for return.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s -owning.override.param=Incompatible ownership for parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s +owning.override.return=Incompatible ownership of return type.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s +owning.override.param=Incompatible ownership of parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java index 644846d85925..6642b6eb3d8d 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java @@ -6,7 +6,7 @@ import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -/** Tests for the Resource Leak Checker. */ +/** Tests for the Resource Leak Checker for Collections. */ public class ResourceLeakCollections extends CheckerFrameworkPerDirectoryTest { public ResourceLeakCollections(List testFiles) { super( diff --git a/checker/tests/resourceleak/EnhancedFor.java b/checker/tests/resourceleak/EnhancedFor.java index 56f251e1e43b..4169d2f48540 100644 --- a/checker/tests/resourceleak/EnhancedFor.java +++ b/checker/tests/resourceleak/EnhancedFor.java @@ -26,9 +26,9 @@ void test2(List<@MustCall Socket> list) { } void test3(List list) { - // With the extension of the RLC to collections, this is no longer an error. - // The return type of Iterator#next is NotOwning, because the iterator does not - // own the elements, but instead the host collection the iterator is associated with. + // With the RLC collection extension, this is no longer an error. + // Iterator#next returns @NotOwning: iterators do not own their elements; ownership + // stays with the host collection associated with the iterator. for (Socket s : list) {} } diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java index d88cda53e23d..7032dbc2bcdc 100644 --- a/checker/tests/resourceleak/Issue4815.java +++ b/checker/tests/resourceleak/Issue4815.java @@ -10,7 +10,7 @@ public void initialize( object.initialize(); // `list` resolves to List<@MustCall Component> and thus cannot accept // an element of non-empty @MustCall type enforced by the Java - // typechecker. This is an unfortunate consequence of the otherwise + // type checker. This is an unfortunate consequence of the otherwise // elegant extension of the RLC to collections, which doesn't detect // that object already fulfilled its obligation here. // :: error: argument diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index 190d89db7a11..3493dba83f42 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -23,10 +23,9 @@ public List test2(@OwningCollection List l) { } public void test3(List<@MustCall({}) Socket> l) throws Exception { - // although s is illegally assigned into the list l, an error - // for required.method.not.called is not additionally reported at - // this declaration site of s. list#add(@Owning E) takes on - // the obligation of the argument. + // although s is illegally added to l, a required.method.not.called error + // is not additionally reported at this declaration site. List#add(@Owning E) takes on + // the obligation of its argument. Socket s = new Socket(); s.bind(new InetSocketAddress("192.168.0.1", 0)); // l cannot hold elements with non-empty @MustCall values diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java index bc5269137acc..4130e06170ba 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java @@ -108,8 +108,8 @@ public interface CFGVisualizer< * element. * * @param ice the iterated collection element - * @param value the value of the local variable - * @return the String representation of the local variable + * @param value the value associated with {@code ice} + * @return the String representation of this entry */ String visualizeStoreIteratedCollectionElt(IteratedCollectionElement ice, V value); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index a2f0ab2bad8d..7e88b6139cfb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -20,12 +20,12 @@ public class IteratedCollectionElement extends JavaExpression { /** * Creates a new IteratedCollectionElement. * - * @param var a CFG node + * @param node CFG node * @param tree an AST tree */ - public IteratedCollectionElement(Node var, Tree tree) { - super(var.getType()); - this.node = var; + public IteratedCollectionElement(Node node, Tree tree) { + super(node.getType()); + this.node = node; this.tree = tree; } diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 0eb7ac826cd9..896bcd3aac06 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -3020,8 +3020,8 @@ protected void checkExceptionParameter(CatchTree tree) { /** * Returns a set of AnnotationMirrors that is a lower bound for exception parameters. If a user - * writes a more specific (lower) annotation that this on an exception parameter, the Checker - * Framework issues a warning saying that the written annotation cannot be verified. + * writes a more specific (lower) annotation than this on an exception parameter, the Checker + * Framework issues a warning indicating that the written annotation cannot be verified. * *

This implementation returns top; subclasses can change this behavior. * @@ -3086,7 +3086,7 @@ protected void checkThrownExpression(ThrowTree tree) { * Returns a set of AnnotationMirrors that is an upper bound for thrown exceptions. * *

Note: by default this method returns {@code getExceptionParameterLowerBoundAnnotations()}, - * so that this annotation is enforced. + * so that the same bound is enforced. * *

(Default is top.) * diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index 586e5e79c6c4..12ced00653a2 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -1238,7 +1238,9 @@ private S upperBound(S other, boolean shouldWiden) { if (thisVal != null) { V otherVal = e.getValue(); V mergedVal = upperBoundOfValues(otherVal, thisVal, shouldWiden); - newStore.iteratedCollectionElements.put(ice, mergedVal); + if (mergedVal != null) { + newStore.iteratedCollectionElements.put(ice, mergedVal); + } } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index b355452f12e2..16bbda59e3e1 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -176,7 +176,7 @@ public static boolean isConstructor(MethodTree tree) { * @return true iff tree describes a call to hasNext */ public static boolean isHasNextCall(MethodInvocationTree tree) { - return isNamedMethodCall("hasNext", tree); + return isNamedMethodCall("hasNext", tree) && tree.getArguments().isEmpty(); } /** @@ -1747,7 +1747,8 @@ && isNamedMethodCall("get", (MethodInvocationTree) tree)) { */ public static boolean isSizeAccess(Tree tree) { return (tree instanceof MethodInvocationTree) - && isNamedMethodCall("size", (MethodInvocationTree) tree); + && isNamedMethodCall("size", (MethodInvocationTree) tree) + && ((MethodInvocationTree) tree).getArguments().isEmpty(); } /** From c2309f04939f3fd6a508b36b8deb08bcb8bab3ff Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 8 Oct 2025 15:14:23 +0200 Subject: [PATCH 330/374] revert error message to previous --- .../checker/rlccalledmethods/messages.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties index 412de3c7dcab..1935440e2db2 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties @@ -1,2 +1,2 @@ -owning.override.return=Incompatible ownership of return type.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s -owning.override.param=Incompatible ownership of parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s +owning.override.return=Incompatible ownership for return.%nfound : no ownership annotation or @Owning%nrequired: @NotOwning%nConsequence: method %s in %s cannot override method %s in %s +owning.override.param=Incompatible ownership for parameter %s.%nfound : no ownership annotation or @NotOwning%nrequired: @Owning%nConsequence: method %s in %s cannot override method %s in %s From 50394ff64573579662a1b91c74affb56ec009bdc Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 8 Oct 2025 21:06:24 -0700 Subject: [PATCH 331/374] Comment improvements --- .../checker/mustcall/MustCallAnnotatedTypeFactory.java | 2 +- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 -- .../checker/resourceleak/ResourceLeakChecker.java | 4 ++-- checker/tests/resourceleak/EnhancedFor.java | 1 - checker/tests/resourceleak/IndexMode.java | 3 +-- checker/tests/resourceleak/Issue6030.java | 7 +++---- checker/tests/resourceleak/SocketIntoList.java | 7 +++---- 7 files changed, 10 insertions(+), 16 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 920d94de3781..b5be59815b4c 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -588,7 +588,7 @@ protected QualifierHierarchy createQualifierHierarchy() { * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code * afterFirstStore} is false, the store before {@code succBlock} is returned. * - * @param afterFirstStore if true, use the store after the first block; if false, use the store + * @param afterFirstStore if true, use the store after {@code firstBlock}; if false, use the store * before its successor, {@code succBlock} * @param firstBlock a CFG block * @param succBlock {@code firstBlock}'s successor diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 6b373a67ac72..78ee6955e8a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2826,8 +2826,6 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { } if (coAtf.isOwningCollectionParameter(paramElement)) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(param); - { - } if (mustCallValues != null) { if (!ResourceLeakUtils.isIterator(paramElement.asType())) { for (String mustCallMethod : mustCallValues) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java index 1b63216ceb12..b474510fcb20 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -34,8 +34,8 @@ * RLCCalledMethodsChecker → MustCallChecker * *

The subchecker hierarchy is a line graph (instead of siblings), since we want them to operate - * on the same cfg (so we can get both a CM, MC, and CO store for a given cfg block), which only - * works if they are in a linear subchecker hierarchy. + * on the same CFG (so we can get a CalledMethods, MustCall, and CollectionOwnership store for a + * given CFG block), which only works if they are in a linear subchecker hierarchy. */ @SupportedOptions({ "permitStaticOwning", diff --git a/checker/tests/resourceleak/EnhancedFor.java b/checker/tests/resourceleak/EnhancedFor.java index 4169d2f48540..a244a9262071 100644 --- a/checker/tests/resourceleak/EnhancedFor.java +++ b/checker/tests/resourceleak/EnhancedFor.java @@ -26,7 +26,6 @@ void test2(List<@MustCall Socket> list) { } void test3(List list) { - // With the RLC collection extension, this is no longer an error. // Iterator#next returns @NotOwning: iterators do not own their elements; ownership // stays with the host collection associated with the iterator. for (Socket s : list) {} diff --git a/checker/tests/resourceleak/IndexMode.java b/checker/tests/resourceleak/IndexMode.java index 522815964139..abd9f0937217 100644 --- a/checker/tests/resourceleak/IndexMode.java +++ b/checker/tests/resourceleak/IndexMode.java @@ -69,8 +69,7 @@ public static void getMode5(Map indexOptions) { } // This variant uses an InputStream (which has a MustCall type by default) as the - // value type in the map. This is not an error anymore, as the values are permitted - // to have any @MustCall type. + // value type in the map. The values are permitted to have any @MustCall type. public static Object getModeIS(Map indexOptions) { try { // Since Map#get returns @NotOwning, this reports no error. diff --git a/checker/tests/resourceleak/Issue6030.java b/checker/tests/resourceleak/Issue6030.java index 082b0003842a..f300b03b8af9 100644 --- a/checker/tests/resourceleak/Issue6030.java +++ b/checker/tests/resourceleak/Issue6030.java @@ -18,10 +18,9 @@ public boolean hasNext(@NotOwningCollection MyScanner this) { return iterator.hasNext(); } - /* - * The @NotOwning annotation is required to be consistent with the superclass implementation. - * The return type of Iterator#next is @NotOwning. Soundness is ensured by the RLC for collections. - */ + // The @NotOwning annotation is required to be consistent with the superclass implementation. + // The return type of Iterator#next is @NotOwning. Soundness is ensured by the RLC for + // collections. public @NotOwning T next(@NotOwningCollection MyScanner this) { return null; } diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index 3493dba83f42..971f98cb7d2a 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -23,7 +23,7 @@ public List test2(@OwningCollection List l) { } public void test3(List<@MustCall({}) Socket> l) throws Exception { - // although s is illegally added to l, a required.method.not.called error + // Although s is illegally added to l, a required.method.not.called error // is not additionally reported at this declaration site. List#add(@Owning E) takes on // the obligation of its argument. Socket s = new Socket(); @@ -35,9 +35,8 @@ public void test3(List<@MustCall({}) Socket> l) throws Exception { // This input list might have been produced by e.g., test1() public void test4(List<@MustCall({}) Socket> l) throws Exception { - // l.get(0) is not an error as List#get returns @NotOwning. - // However, s.bind tries to reset the mustcall obligations of s, - // which is only permitted if s is owning. + // l.get(0) is not an error as List#get returns @NotOwning. However, s.bind tries + // to reset the MustCall obligations of s, which is only permitted if s is owning. Socket s = l.get(0); // :: error: reset.not.owning s.bind(new InetSocketAddress("192.168.0.1", 0)); From 0faec762f4d0347697f9cbd477f1118e13e3f276 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Wed, 19 Nov 2025 09:28:16 +0100 Subject: [PATCH 332/374] fix compilation error in rlccmatf --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 30c0ccc92a71..a676502fd303 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -39,6 +39,7 @@ import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; +import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; @@ -46,6 +47,7 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; @@ -229,7 +231,8 @@ protected ControlFlowGraph analyze( capturedStore); assert root != null : "@AssumeAssertion(nullness): at this point root is always nonnull"; rlc.setRoot(root); - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(rlc); + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = + new MustCallConsistencyAnalyzer(rlc, false); mustCallConsistencyAnalyzer.analyze(cfg); // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for From a8d9fcac90bac4b02fb7c4cf6187c1596bdc4b22 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 5 Dec 2025 21:00:51 +0100 Subject: [PATCH 333/374] fix up rlc4c manual section --- docs/manual/resource-leak-checker.tex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index dbcdaf09998e..9adc16593376 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -652,7 +652,7 @@ Towards that end, the Resource Leak Checker tracks every allocated resource collection in two ways: \begin{itemize} - \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - for example, they can't be used to add or remove elements to and from the collection, respectively. + \item at most one owning reference for each underlying resource collection is tracked via an ownership type system. This owning reference may arbitrarily mutate the collection. All other references are considered not owning and are restricted - for example, they can't be used to add elements to or remove elements from the collection. \item a complementary dataflow analysis tracks an obligation for each allocated resource collection. The obligation can be passed on to method parameters, fields, or return values for example, just like the ownership. The difference is that while the type system tracks at most one owner per resource collection allowed to mutate it without restriction, the dataflow analysis tracks at least one obligation per resource collection to ensure it is definitely fulfilled. \end{itemize} @@ -732,7 +732,7 @@ try { s.close(); } catch (Exception e) { - System.out.println(e.stackTrace()); + System.out.println(e.printStackTrace()); } } // `sockets` is @OwningCollectionWithoutObligation again From 43dcf8e1f008b3ed2dbaf325f23450fd1276f1b2 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 5 Dec 2025 21:10:51 +0100 Subject: [PATCH 334/374] fix typo in error message --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 293132944174..0b29622663f7 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -2551,7 +2551,7 @@ private void propagateObligationsToSuccessorBlock( // immediately. throw new InvalidLoopBodyAnalysisException("Block with no incoming store."); } else { - throw new BugInCF("block with no outgoing incoming store: " + successor); + throw new BugInCF("block with no incoming store: " + successor); } } From 5956dd24bc4ebfd0ba470a417758bc8e33faec61 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Fri, 5 Dec 2025 21:11:23 +0100 Subject: [PATCH 335/374] nullness guard when checking owning collection field --- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 0b29622663f7..4241df58d635 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1394,7 +1394,9 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg, ReturnNode nod return false; } else { // return is @OwningCollection. Report error if @OwningCollection field. - if (coAtf.isOwningCollectionField(TreeUtils.elementFromTree(node.getResult().getTree()))) { + if (node.getResult() != null + && coAtf.isOwningCollectionField( + TreeUtils.elementFromTree(node.getResult().getTree()))) { checker.reportError( node.getTree(), "transfer.owningcollection.field.ownership", From e99fdb7f0d7dfbee63467c88f21af96445605b93 Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 11 Jan 2026 18:56:00 +0100 Subject: [PATCH 336/374] remove old manual section about missing rlc for collections --- docs/manual/resource-leak-checker.tex | 43 --------------------------- 1 file changed, 43 deletions(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index 9adc16593376..b0179c2a062f 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -957,49 +957,6 @@ \subsectionAndLabel{JDK Method Signatures}{resource-leak-collections-jdk-methods} [[TODO: Add a table with a number of collection/iterable methods and their collection ownership annotations]] - -% The Resource Leak Checker cannot verify code that stores a collection of -% resources in a generic collection (e.g., \) and then -% resolves the obligations of each element of the collection at once (e.g., -% by iterating over the \). In the future, the checker will support -% this. The remainder of this section explains the implementation issues; -% most users can skip it. - -% The first implementation issue is that \<@Owning> and \<@NotOwning> are -% declaration annotations rather than type qualifiers, so they cannot be -% written on type arguments. It is possible under the current design to have -% an \code{@Owning List}, but not a \code{List<@Owning Socket>}. -% It would be better to make \<@Owning> a type annotation, but this is a -% challenging design problem. - -% The second implementation issue is the defaulting rule for \<@MustCall> on -% type variable upper bounds. Currently, this default is \<@MustCall(\{\})>, -% which prevents many false positives in code with type variables that makes -% no use of resources --- an important design principle. -% However, this defaulting rule does have an unfortunate consequence: it is -% an error to write \code{List} or any other type with a concrete -% type argument where the type argument itself isn't \<@MustCall(\{\})>. A programmer who -% needs to write such a type while using the Resource Leak Checker has a few -% choices, all of which have some downsides: - -% \begin{itemize} -% \item Write \code{List}. This rejects calls to \ -% or other methods that require an instance of the type variable, but it -% preserves some of the behavior (e.g., calls to \ are permitted). -% This is the best choice most of the time if the \ is not intended to -% be owning. -% \item Write \code{List<@MustCall Socket>}. This makes it an error to -% add a Socket to the list, since the type of the Socket is -% \<@MustCall("close")> but the list requires \<@MustCall()>. -% \item Suppress one or more warnings. -% \end{itemize} - -% The recommended way to use the Resource Leak Checker in this situation is -% to rewrite the code to avoid a \ of owning resources. If rewriting is -% not possible, the programmer will probably need to suppress a warning and -% then verify the code using a method other than the Resource Leak Checker. - - \sectionAndLabel{Further reading}{resource-leak-checker-references} The paper ``Lightweight and Modular Resource Leak From 5d9dc7243bf1fbd850876b43aff7e01b1092712b Mon Sep 17 00:00:00 2001 From: Sascha Kehrli Date: Sun, 11 Jan 2026 18:56:13 +0100 Subject: [PATCH 337/374] fix error in example program in manual --- docs/manual/resource-leak-checker.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/resource-leak-checker.tex b/docs/manual/resource-leak-checker.tex index b0179c2a062f..cd5644f0c3d4 100644 --- a/docs/manual/resource-leak-checker.tex +++ b/docs/manual/resource-leak-checker.tex @@ -732,7 +732,7 @@ try { s.close(); } catch (Exception e) { - System.out.println(e.printStackTrace()); + e.printStackTrace(); } } // `sockets` is @OwningCollectionWithoutObligation again From 57985baed9f2dbb852318a959cfad9e3407a2b6b Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 11:56:13 -0800 Subject: [PATCH 338/374] Put error key in brackets --- checker/tests/README.md | 6 ++-- .../ConversionCategoryTest.java | 8 ++--- checker/tests/i18n-formatter/HasFormat.java | 12 +++---- checker/tests/i18n-formatter/I18nFormat.java | 12 +++---- .../i18n-formatter/I18nFormatForTest.java | 20 +++++------ checker/tests/i18n-formatter/IsFormat.java | 12 +++---- .../ManualExampleI18nFormatter.java | 10 +++--- checker/tests/i18n-formatter/Syntax.java | 34 +++++++++---------- .../dependenttypes/DependentTypesError.java | 2 +- .../tests/h1h2checker/TypeRefinement.java | 8 ++--- .../tests/reportmodifiers/TestModifiers.java | 2 +- .../tests/reporttreekinds/TestTreeKinds.java | 2 +- 12 files changed, 64 insertions(+), 64 deletions(-) diff --git a/checker/tests/README.md b/checker/tests/README.md index 6c9f75a50f79..9a61ee97e3e2 100644 --- a/checker/tests/README.md +++ b/checker/tests/README.md @@ -112,14 +112,14 @@ or by creating the test and observing the failure. To indicate the expected failure, insert the line ```java - // :: error: () + // :: error: [] ``` directly preceding the expected error line. If a warning rather than an error is expected, insert the line ```java - // :: warning: () + // :: warning: [] ``` If a stub parser warning is expected, insert the line @@ -132,7 +132,7 @@ If multiple errors are expected on a single line, duplicate everything except the "//" comment characters, as in ```java - // :: error: () :: error: () + // :: error: [] :: error: [] ``` If the expected failures line would be very long, you may break it across diff --git a/checker/tests/i18n-formatter/ConversionCategoryTest.java b/checker/tests/i18n-formatter/ConversionCategoryTest.java index fa49238c61b2..80bf3394848b 100644 --- a/checker/tests/i18n-formatter/ConversionCategoryTest.java +++ b/checker/tests/i18n-formatter/ConversionCategoryTest.java @@ -10,7 +10,7 @@ public static void main(String[] args) { @I18nFormat({I18nConversionCategory.NUMBER}) String s3 = "{0, number}"; @I18nFormat({I18nConversionCategory.NUMBER, I18nConversionCategory.NUMBER}) String s4 = "{1} {0, date}"; - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] s4 = "{0}"; @I18nFormat({I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER}) String s5 = "{0} and {1, number}"; @@ -48,7 +48,7 @@ public static void main(String[] args) { @I18nFormat({I18nConversionCategory.DATE}) String s13 = "{0, date} {0, date}"; - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] @I18nFormat({I18nConversionCategory.GENERAL}) String b1 = "{1}"; // :: error: [assignment] @@ -60,10 +60,10 @@ public static void main(String[] args) { // :: error: [assignment] @I18nFormat({I18nConversionCategory.GENERAL}) String b4 = "{0, date}"; - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] @I18nFormat({I18nConversionCategory.DATE}) String b5 = "{0, date} {1, date}"; - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] @I18nFormat({I18nConversionCategory.DATE, I18nConversionCategory.DATE}) String b6 = "{0, date}"; } } diff --git a/checker/tests/i18n-formatter/HasFormat.java b/checker/tests/i18n-formatter/HasFormat.java index 6cb59928deeb..007519794a23 100644 --- a/checker/tests/i18n-formatter/HasFormat.java +++ b/checker/tests/i18n-formatter/HasFormat.java @@ -9,11 +9,11 @@ void test1(String format) { if (I18nFormatUtil.hasFormat( format, I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER)) { MessageFormat.format(format, "S", 1); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format(format, "S"); // :: error: [argument] MessageFormat.format(format, "S", "S"); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format(format, "S", 1, 2); } } @@ -21,7 +21,7 @@ void test1(String format) { void test2(String format) { if (!I18nFormatUtil.hasFormat( format, I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, "S", 1); } } @@ -32,13 +32,13 @@ void test3(String format) { I18nConversionCategory.GENERAL, I18nConversionCategory.UNUSED, I18nConversionCategory.GENERAL)) { - // :: warning: (i18nformat.argument.unused) + // :: warning: [i18nformat.argument.unused] MessageFormat.format(format, "S", 1, "S"); } } void test4(String format) throws Exception { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, "S"); if (I18nFormatUtil.hasFormat(format, I18nConversionCategory.GENERAL)) { MessageFormat.format(format, "S"); @@ -55,7 +55,7 @@ void tes5(String format) { MessageFormat.format(format, "S"); MessageFormat.format(format, 1); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(format, 1); } } diff --git a/checker/tests/i18n-formatter/I18nFormat.java b/checker/tests/i18n-formatter/I18nFormat.java index 19f57d25cf9c..1232f963cb14 100644 --- a/checker/tests/i18n-formatter/I18nFormat.java +++ b/checker/tests/i18n-formatter/I18nFormat.java @@ -11,24 +11,24 @@ void test() { MessageFormat.format("{0, number}{1, number}", 1, 2); MessageFormat.format("{0, number}{0}", 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0, number}'", new Date(12)); // Using {44444} here would yield: "invalid format string 44444 exceeds // the ArgumentIndex implementation limit". The hard limit is 10000. - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format("''{0, time, short}''{1}{2, time} {33, number}{4444}'{''''", 0); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] MessageFormat.format("{0, number}{1, number}", 1); - // :: warning: (i18nformat.argument.unused) + // :: warning: [i18nformat.argument.unused] MessageFormat.format("{1, number}", 1, 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("{0, number}", 1, new Date()); - // :: warning: (i18nformat.indirect.arguments) + // :: warning: [i18nformat.indirect.arguments] MessageFormat.format("{0, number}", new Object[2]); MessageFormat.format("{0}", "S"); diff --git a/checker/tests/i18n-formatter/I18nFormatForTest.java b/checker/tests/i18n-formatter/I18nFormatForTest.java index 7a0a86e3cbb6..83eff2df7262 100644 --- a/checker/tests/i18n-formatter/I18nFormatForTest.java +++ b/checker/tests/i18n-formatter/I18nFormatForTest.java @@ -12,41 +12,41 @@ public static void main(String[] args) { A a1 = new A(); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] a1.methodA("{0, number", new Date(12)); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] a1.methodA("'{0{}", 1); a1.methodA("{0}", "A"); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] a(1, 1.2, "{0, number", 1.2, new Date(12)); a(1, 1.2, "{0, number}{1}", 1.2, 1, "A"); - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] a(1, 1.2, "{0, number}{1}", 1.2, 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] a(1, 1.2, "{0, number}{1}", 1.2, 1, "A", 2); b("{0, number}{1}", 1, "A"); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] b("{0, number", new Date(12)); b("{0, number}{1}", 1, "A"); b("{0}", "a string"); // :: error: [argument] b("{0, number}", "a string"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] c("{0, number}{1}", 1, "A"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] e(1, 2); f("{0}", 2); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] h("{0}", "a string"); - // :: error: (i18nformat.formatfor) + // :: error: [i18nformat.formatfor] i("{0}", "a string"); j("{0}"); diff --git a/checker/tests/i18n-formatter/IsFormat.java b/checker/tests/i18n-formatter/IsFormat.java index 129575397482..64a71952ce75 100644 --- a/checker/tests/i18n-formatter/IsFormat.java +++ b/checker/tests/i18n-formatter/IsFormat.java @@ -5,15 +5,15 @@ public class IsFormat { public static void test1(String cc) { if (!I18nFormatUtil.isFormat(cc)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); if (I18nFormatUtil.hasFormat(cc, I18nConversionCategory.GENERAL)) { MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } } @@ -21,15 +21,15 @@ public static void test1(String cc) { public static void test2(String cc) { if (!I18nFormatUtil.isFormat(cc)) { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); if (I18nFormatUtil.hasFormat(cc, I18nConversionCategory.NUMBER)) { MessageFormat.format(cc, 1); } else { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format(cc, "A"); } } diff --git a/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java b/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java index f124f67deca3..3c14a7c76267 100644 --- a/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java +++ b/checker/tests/i18n-formatter/ManualExampleI18nFormatter.java @@ -14,24 +14,24 @@ void m(boolean flag) { f = "{0, number, #.#} {1, date}"; // OK f = "{0, number} {1}"; // OK, GENERAL is weaker (less restrictive) than DATE f = "{0} {1, date}"; // OK, GENERAL is weaker (less restrictive) than NUMBER - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0, number}"; // warning: last argument is ignored - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0}"; // warning: last argument is ignored - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = flag ? "{0, number} {1}" : "{0, number}"; if (flag) { f = "{0, number} {1}"; } else { - // :: warning: (i18nformat.missing.arguments) + // :: warning: [i18nformat.missing.arguments] f = "{0, number}"; } @I18nFormat({NUMBER, DATE}) String f2 = f; // :: error: [assignment] f = "{0, number} {1, number}"; // error: NUMBER is stronger (more restrictive) than DATE - // :: error: (i18nformat.excess.arguments) :: error: [assignment] + // :: error: [i18nformat.excess.arguments] :: error: [assignment] f = "{0} {1} {2}"; // error: too many arguments } } diff --git a/checker/tests/i18n-formatter/Syntax.java b/checker/tests/i18n-formatter/Syntax.java index ff67f839a5eb..68a24fc1eb24 100644 --- a/checker/tests/i18n-formatter/Syntax.java +++ b/checker/tests/i18n-formatter/Syntax.java @@ -5,35 +5,35 @@ public class Syntax { // Test 2.1.1: Missing '}' at end of message format (Unmatched braces in the pattern) public static void unmatchedBraces() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number", new Date(12)); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0}{", 1); // good - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0{}", 1); - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{0{}'", 1); } // Test 2.1.2.1: The argument number needs to be an integer public static void integerRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{{0}}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0.2}", 1); // good - // :: warning: (i18nformat.excess.arguments) + // :: warning: [i18nformat.excess.arguments] MessageFormat.format("'{{0}}'", 1); } // Test 2.1.2.2: The argument number can't be negative public static void nonNegativeRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{-1, number}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{-123}", 1); // good @@ -42,7 +42,7 @@ public static void nonNegativeRequired() { // Test 2.1.3: Format Style required for choice format public static void formatStyleRequired() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, choice}", 1); // good @@ -51,9 +51,9 @@ public static void formatStyleRequired() { // Test 2.1.4: Wrong format Style public static void wrongFormatStyle() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, time, number}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number, y.m.d}", 1); // good @@ -63,9 +63,9 @@ public static void wrongFormatStyle() { // Test 2.1.5: Unknown format type public static void unknownFormatType() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, general}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, fool}", 1); // good @@ -79,12 +79,12 @@ public static void unknownFormatType() { // Test 2.1.6: Invalid Subformat Pattern public static void invalidSubformatPattern() { - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, number, #.#.#}", 1); - // :: error: (i18nformat.string) + // :: error: [i18nformat.string] MessageFormat.format("{0, date, y.m.d.x}", new Date()); // This seems to be permitted by Java 23+. - // // :: error: (i18nformat.string) + // // :: error: [i18nformat.string] // MessageFormat.format("{0, choice, 0##zero}", 0); // good diff --git a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java index bdbc72d89ff0..4508cd4707fa 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java +++ b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java @@ -25,7 +25,7 @@ public class DependentTypesError { /** Regular expression for unparsing string representations of this class (gross). */ private static final Pattern ERROR_PATTERN = - Pattern.compile("\\[error for expression: (.*); error: (.*)\\]"); + Pattern.compile("\\[error for expression: (.*); error: [.*]\\]"); /** * Returns true if the given expression string is an error. That is, whether it is a string that diff --git a/framework/tests/h1h2checker/TypeRefinement.java b/framework/tests/h1h2checker/TypeRefinement.java index 682e865d37e9..f910e1a1bbd9 100644 --- a/framework/tests/h1h2checker/TypeRefinement.java +++ b/framework/tests/h1h2checker/TypeRefinement.java @@ -4,14 +4,14 @@ public class TypeRefinement { // :: warning: [cast.unsafe.constructor.invocation] @H1Top Object o = new @H1S1 Object(); - // :: error: (h1h2checker.h1invalid.forbidden) :: warning: [cast.unsafe.constructor.invocation] + // :: error: [h1h2checker.h1invalid.forbidden] :: warning: [cast.unsafe.constructor.invocation] @H1Top Object o2 = new @H1Invalid Object(); - // :: error: (h1h2checker.h1invalid.forbidden) + // :: error: [h1h2checker.h1invalid.forbidden] @H1Top Object o3 = getH1Invalid(); - // :: error: (h1h2checker.h1invalid.forbidden) + // :: error: [h1h2checker.h1invalid.forbidden] @H1Invalid Object getH1Invalid() { - // :: error: (h1h2checker.h1invalid.forbidden) :: warning: + // :: error: [h1h2checker.h1invalid.forbidden] :: warning: // (cast.unsafe.constructor.invocation) return new @H1Invalid Object(); } diff --git a/framework/tests/reportmodifiers/TestModifiers.java b/framework/tests/reportmodifiers/TestModifiers.java index f645645aa398..cde23a4c51c8 100644 --- a/framework/tests/reportmodifiers/TestModifiers.java +++ b/framework/tests/reportmodifiers/TestModifiers.java @@ -5,7 +5,7 @@ public class TestModifiers { void test() { class Inner { - // :: error: (Modifier.native) + // :: error: [Modifier.native] native void bad(); } } diff --git a/framework/tests/reporttreekinds/TestTreeKinds.java b/framework/tests/reporttreekinds/TestTreeKinds.java index dcf18aeb01f6..d191a7f27cc2 100644 --- a/framework/tests/reporttreekinds/TestTreeKinds.java +++ b/framework/tests/reporttreekinds/TestTreeKinds.java @@ -4,7 +4,7 @@ */ public class TestTreeKinds { void test(boolean a, boolean b) { - // :: error: (Tree.Kind.WHILE_LOOP) :: error: (Tree.Kind.CONDITIONAL_AND) + // :: error: [Tree.Kind.WHILE_LOOP] :: error: [Tree.Kind.CONDITIONAL_AND] while (a && b) {} if (b) {} } From 7668ccc633ff7e99503bc5c5dbd9935485988357 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 13:36:28 -0800 Subject: [PATCH 339/374] Undo a change --- .../framework/util/dependenttypes/DependentTypesError.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java index 4508cd4707fa..bdbc72d89ff0 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java +++ b/framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesError.java @@ -25,7 +25,7 @@ public class DependentTypesError { /** Regular expression for unparsing string representations of this class (gross). */ private static final Pattern ERROR_PATTERN = - Pattern.compile("\\[error for expression: (.*); error: [.*]\\]"); + Pattern.compile("\\[error for expression: (.*); error: (.*)\\]"); /** * Returns true if the given expression string is an error. That is, whether it is a string that From cb5f029275bd70f3ee15a03180a7fdc1ffb969e8 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 13:51:06 -0800 Subject: [PATCH 340/374] Another pair of brackets --- framework/tests/h1h2checker/TypeRefinement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/tests/h1h2checker/TypeRefinement.java b/framework/tests/h1h2checker/TypeRefinement.java index f910e1a1bbd9..02147e9f5114 100644 --- a/framework/tests/h1h2checker/TypeRefinement.java +++ b/framework/tests/h1h2checker/TypeRefinement.java @@ -12,7 +12,7 @@ public class TypeRefinement { // :: error: [h1h2checker.h1invalid.forbidden] @H1Invalid Object getH1Invalid() { // :: error: [h1h2checker.h1invalid.forbidden] :: warning: - // (cast.unsafe.constructor.invocation) + // [cast.unsafe.constructor.invocation] return new @H1Invalid Object(); } } From 775a2dbcaef432832467c4d94d64941a9f4fb178 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 2 Mar 2026 18:13:12 -0800 Subject: [PATCH 341/374] Add space around heading --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b6e97214b966..8701e8ab92ed 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,7 @@ + ## Version 3.54.1 (2026-04-02) ### User-visible changes From ffda69be096c5bbd988ad40eede09154c0bdc5b7 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 22 Mar 2026 19:54:45 -0700 Subject: [PATCH 342/374] Add pre-lambda post-analysis hook for resource leak --- ...llectionOwnershipAnnotatedTypeFactory.java | 25 +++++++- .../MustCallConsistencyAnalyzer.java | 4 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 58 ------------------- .../type/GenericAnnotatedTypeFactory.java | 14 +++++ 4 files changed, 39 insertions(+), 62 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 809923241460..226945fecc98 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -11,6 +11,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -99,6 +100,13 @@ public class CollectionOwnershipAnnotatedTypeFactory private final ExecutableElement collectionFieldDestructorValueElement = TreeUtils.getMethod(CollectionFieldDestructor.class, "value", 0, processingEnv); + /** + * Method CFGs whose resource-leak post-analysis already ran before contained lambdas were + * analyzed. + */ + private final Set preLambdaPostAnalyzedMethods = + Collections.newSetFromMap(new IdentityHashMap<>()); + /** * Enum for the types in the hierarchy. Combined with a few utility methods to get the right enum * value from various sources, this is a convenient interface to deal with annotations in this @@ -220,8 +228,23 @@ public CollectionOwnershipStore getStoreForBlock( // typechecker runs last in the RLC, not because this has anything to do with // collections. Whatever checker runs last in the RLC must do this. TODO: make this // run last in a more sensible way. + @Override + protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { + runResourceLeakPostAnalyze(cfg); + preLambdaPostAnalyzedMethods.add(cfg); + } + @Override public void postAnalyze(ControlFlowGraph cfg) { + if (!preLambdaPostAnalyzedMethods.remove(cfg)) { + runResourceLeakPostAnalyze(cfg); + } + + super.postAnalyze(cfg); + } + + /** Runs the resource-leak-specific post-analysis that must happen in the last checker. */ + private void runResourceLeakPostAnalyze(ControlFlowGraph cfg) { ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); rlc.setRoot(root); MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = @@ -237,8 +260,6 @@ public void postAnalyze(ControlFlowGraph cfg) { MustCallInference.runMustCallInference(cmAtf, cfg, mustCallConsistencyAnalyzer); } } - - super.postAnalyze(cfg); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4241df58d635..1e63d4bc3ef8 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1395,8 +1395,8 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg, ReturnNode nod } else { // return is @OwningCollection. Report error if @OwningCollection field. if (node.getResult() != null - && coAtf.isOwningCollectionField( - TreeUtils.elementFromTree(node.getResult().getTree()))) { + && coAtf.isOwningCollectionField( + TreeUtils.elementFromTree(node.getResult().getTree()))) { checker.reportError( node.getTree(), "transfer.owningcollection.field.ownership", diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 1d060c9babd4..722677e4e3fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -2,9 +2,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; @@ -15,7 +13,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Queue; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -39,7 +36,6 @@ import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; -import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; @@ -47,13 +43,11 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; -import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.framework.flow.CFAbstractAnalysis.FieldInitialValue; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -63,7 +57,6 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; -import org.plumelib.util.IPair; /** * The type factory for the RLCCalledMethodsChecker. The main difference between this and the Called @@ -197,57 +190,6 @@ public AnnotationMirror createCalledMethods(String... val) { return createAccumulatorAnnotation(Arrays.asList(val)); } - @Override - protected ControlFlowGraph analyze( - Queue> classQueue, - Queue> lambdaQueue, - UnderlyingAST ast, - List> fieldValues, - @Nullable ControlFlowGraph cfg, - boolean isInitializationCode, - boolean updateInitializationStore, - boolean isStatic, - @Nullable AccumulationStore capturedStore) { - // This is a workaround for a bug that I tried and failed to fix. - // See checker/tests/resourceleak/RLLambda.java. - // This code really belongs in postAnalyze, but this code only works correctly when called after - // a method is analyzed the first time and before any containing lambdas are analyzed. - // This workaround means there could be false positives when the type of a method invocation - // depends on dataflow in a lambda. - - if (cfg != null) { - // The cfg is not null, so the analysis has been run before. Don't rerun it. - return cfg; - } - cfg = - super.analyze( - classQueue, - lambdaQueue, - ast, - fieldValues, - cfg, - isInitializationCode, - updateInitializationStore, - isStatic, - capturedStore); - assert root != null : "@AssumeAssertion(nullness): at this point root is always nonnull"; - rlc.setRoot(root); - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer(rlc, false); - mustCallConsistencyAnalyzer.analyze(cfg); - - // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for - // finalizer methods and @InheritableMustCall annotations for the class declarations. - if (getWholeProgramInference() != null) { - if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { - MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); - } - } - - tempVarToTree.clear(); - return cfg; - } - @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); 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 c8f0a6294f74..4d83606f4e3f 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1523,6 +1523,9 @@ private void performFlowAnalysisForMethod( /* updateInitializationStore= */ false, /* isStatic= */ false, capturedStore); + if (firstIteration) { + postAnalyzeAfterFirstMethodAnalysis(methodCFG); + } boolean anyLambdaResultChanged = false; while (!lambdaQueueInMethod.isEmpty()) { IPair lambdaPair = lambdaQueueInMethod.remove(); @@ -1589,6 +1592,17 @@ private boolean containsAllVoidLambdas(Set lambdas) { return true; } + /** + * Perform any additional operations on a method CFG after its first analysis and before any + * contained lambdas are analyzed. + * + *

This hook is invoked once per method CFG. If the method contains no lambdas, then this hook + * is called after the first analysis and before {@link #postAnalyze(ControlFlowGraph)}. + * + * @param cfg the method CFG + */ + protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) {} + /** Sorts a list of trees with the variables first. */ private final Comparator sortVariablesFirst = (t1, t2) -> { From 0082c054ffab547455d3075d4c2ec2dfddd9ca3e Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Mon, 23 Mar 2026 12:45:58 -0700 Subject: [PATCH 343/374] Preserve RLCC method analysis during lambda fixpoint --- .../RLCCalledMethodsAnnotatedTypeFactory.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 722677e4e3fe..91e0a3daf930 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -2,7 +2,9 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; @@ -13,6 +15,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -43,11 +46,13 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.framework.flow.CFAbstractAnalysis.FieldInitialValue; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -57,6 +62,7 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; +import org.plumelib.util.IPair; /** * The type factory for the RLCCalledMethodsChecker. The main difference between this and the Called @@ -190,6 +196,40 @@ public AnnotationMirror createCalledMethods(String... val) { return createAccumulatorAnnotation(Arrays.asList(val)); } + @Override + protected ControlFlowGraph analyze( + Queue> classQueue, + Queue> lambdaQueue, + UnderlyingAST ast, + List> fieldValues, + @Nullable ControlFlowGraph cfg, + boolean isInitializationCode, + boolean updateInitializationStore, + boolean isStatic, + @Nullable AccumulationStore capturedStore) { + if (cfg != null && ast.getKind() == UnderlyingAST.Kind.METHOD) { + // Preserve the first method analysis result, but keep the framework's lambda loop running + // by re-enqueuing the nested classes and lambdas from the existing CFG. + for (ClassTree cls : cfg.getDeclaredClasses()) { + classQueue.add(IPair.of(cls, getStoreBefore(cls))); + } + for (LambdaExpressionTree lambda : cfg.getDeclaredLambdas()) { + lambdaQueue.add(IPair.of(lambda, getStoreBefore(lambda))); + } + return cfg; + } + return super.analyze( + classQueue, + lambdaQueue, + ast, + fieldValues, + cfg, + isInitializationCode, + updateInitializationStore, + isStatic, + capturedStore); + } + @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); From e71b956d52ea68fe35b5475ffe3b8316e50fadcf Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Wed, 25 Feb 2026 19:21:16 -0800 Subject: [PATCH 344/374] More loop support. --- .../CollectionOwnershipAnalysis.java | 42 ++ .../CollectionOwnershipTransfer.java | 16 + .../checker/mustcall/MustCallVisitor.java | 487 +++++++++++++++++- .../MustCallConsistencyAnalyzer.java | 88 +++- .../RLCCalledMethodsAnnotatedTypeFactory.java | 207 +++++++- .../resourceleak-collections/FinallyBug.java | 28 + .../GraphFoundationCrash.java | 13 + .../LoopBodyAnalysisTest.java | 1 + .../MissingAnnotations.java | 53 ++ .../tests/resourceleak-collections/Test.java | 83 +++ .../WhileLoopDestructor.java | 143 +++++ checker/tests/resourceleak/TempTest.java | 21 + 12 files changed, 1176 insertions(+), 6 deletions(-) create mode 100644 checker/tests/resourceleak-collections/FinallyBug.java create mode 100644 checker/tests/resourceleak-collections/GraphFoundationCrash.java create mode 100644 checker/tests/resourceleak-collections/MissingAnnotations.java create mode 100644 checker/tests/resourceleak-collections/Test.java create mode 100644 checker/tests/resourceleak-collections/WhileLoopDestructor.java create mode 100644 checker/tests/resourceleak/TempTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index 14a4fe97b6b6..bdca2c90de91 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -1,6 +1,10 @@ package org.checkerframework.checker.collectionownership; +import com.google.common.collect.ImmutableSet; +import java.io.UnsupportedEncodingException; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.checker.resourceleak.SetOfTypes; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -16,6 +20,39 @@ public class CollectionOwnershipAnalysis extends CFAbstractAnalysis { + /** + * The set of exceptions to ignore, cached from {@link + * ResourceLeakChecker#getIgnoredExceptions()}. + */ + private static final SetOfTypes DEFAULT_IGNORED_EXCEPTIONS = + SetOfTypes.anyOfTheseNames( + ImmutableSet.of( + // Any method call has a CFG edge for Throwable/RuntimeException/Error + // to represent run-time misbehavior. Ignore it. + Throwable.class.getCanonicalName(), + Error.class.getCanonicalName(), + RuntimeException.class.getCanonicalName(), + // Use the Nullness Checker to prove this won't happen. + NullPointerException.class.getCanonicalName(), + // These errors can't be predicted statically, so ignore them and assume + // they won't happen. + ClassCircularityError.class.getCanonicalName(), + ClassFormatError.class.getCanonicalName(), + NoClassDefFoundError.class.getCanonicalName(), + OutOfMemoryError.class.getCanonicalName(), + // It's not our problem if the Java type system is wrong. + ClassCastException.class.getCanonicalName(), + // It's not our problem if the code is going to divide by zero. + ArithmeticException.class.getCanonicalName(), + // Use the Index Checker to prevent these errors. + ArrayIndexOutOfBoundsException.class.getCanonicalName(), + NegativeArraySizeException.class.getCanonicalName(), + // Most of the time, this exception is infeasible, as the charset used + // is guaranteed to be present by the Java spec (e.g., "UTF-8"). + // Eventually, this exclusion could be refined by looking at the charset + // being requested. + UnsupportedEncodingException.class.getCanonicalName())); + /** * Creates a new {@link CollectionOwnershipAnalysis}. * @@ -46,4 +83,9 @@ public CollectionOwnershipStore createCopiedStore(CollectionOwnershipStore s) { public CFValue createAbstractValue(AnnotationMirrorSet annotations, TypeMirror underlyingType) { return getCfValue(this, annotations, underlyingType); } + + @Override + public boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return DEFAULT_IGNORED_EXCEPTIONS.contains(getTypes(), exceptionType); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index cff2f91a6e3d..e8b8b1323582 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -17,6 +17,8 @@ import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.ConditionalNotNode; +import org.checkerframework.dataflow.cfg.node.GreaterThanNode; import org.checkerframework.dataflow.cfg.node.LessThanNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; @@ -175,6 +177,20 @@ public TransferResult visitMethodInvocation( return res; } + @Override + public TransferResult visitConditionalNot( + ConditionalNotNode node, TransferInput in) { + TransferResult res = super.visitConditionalNot(node, in); + return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + } + + @Override + public TransferResult visitGreaterThan( + GreaterThanNode node, TransferInput in) { + TransferResult res = super.visitGreaterThan(node, in); + return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + } + /** * Executes collection ownership transfer in method invocations. Owning arguments to an owning * parameter lose ownership. diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index b7bac877000d..5f206b37f80a 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -20,9 +20,12 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WhileLoopTree; import com.sun.source.util.TreeScanner; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -32,6 +35,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; import org.checkerframework.checker.mustcall.qual.MustCall; @@ -46,6 +50,7 @@ import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -372,6 +377,472 @@ public Void visitForLoop(ForLoopTree tree, Void p) { return super.visitForLoop(tree, p); } + // /////////////////////////////////////////////////////////////////////////// + // AST-only while-loop matching + // + // We only pattern-match on AST here and record a "pendingCfg" loop. + // Later, when CFG exists, we resolve loopUpdateBlock/bodyEntry/conditional/etc. + // /////////////////////////////////////////////////////////////////////////// + @Override + public Void visitWhileLoop(WhileLoopTree tree, Void p) { + detectCollectionObligationFulfillingWhileLoop(tree); + return super.visitWhileLoop(tree, p); + } + + /** Condition-kind -> allowed extraction methods. */ + private static final class WhileSpec { + final Set extractMethods; + + WhileSpec(Set extractMethods) { + this.extractMethods = extractMethods; + } + } + + // Iterator: while (it.hasNext()) { ... it.next() ... } + private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); + + // Queue/Deque/Stack: while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... } + // Also size() > 0 / 0 < size() + private static final WhileSpec NONEMPTY_SPEC = + new WhileSpec( + new HashSet<>( + Arrays.asList( + // Queue/Deque (null-returning) + "poll", + "pollFirst", + "pollLast", + // Deque (throws on empty, but guarded by condition) + "remove", + "removeFirst", + "removeLast", + // Stack + "pop" + // If you want more later, add "getFirst"/"getLast" only if you also + // guard soundness (they don't remove elements) + ))); + + private static final class WhileHeaderMatch { + final ExpressionTree collectionTree; // the owning collection expression to mark + final @Nullable Name collectionVarNameForBailout; // for writes/bailouts + final Name headerVar; // iterator var (it) OR collection var (q) + final WhileSpec spec; + + WhileHeaderMatch( + ExpressionTree collectionTree, + @Nullable Name collectionVarNameForBailout, + Name headerVar, + WhileSpec spec) { + this.collectionTree = collectionTree; + this.collectionVarNameForBailout = collectionVarNameForBailout; + this.headerVar = headerVar; + this.spec = spec; + } + } + + private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { + // 1) Match header + ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); + WhileHeaderMatch header = matchWhileHeader(condNoParens); + if (header == null) { + return; + } + + // 2) Extract body statements + List bodyStmts; + if (tree.getStatement() instanceof BlockTree) { + bodyStmts = ((BlockTree) tree.getStatement()).getStatements(); + } else if (tree.getStatement() != null) { + bodyStmts = Collections.singletonList(tree.getStatement()); + } else { + return; + } + + // 3) Find exactly one extraction call in the body (soundness) + BodyExtraction extraction = + findSingleExtractionInWhileBody( + bodyStmts, + header.headerVar, + header.collectionVarNameForBailout, + header.spec.extractMethods); + if (extraction == null) { + return; + } + + // Resolve CFG-local metadata *now* (except loopUpdateBlock). + Block condBlock = firstBlockForTree(condNoParens); + if (condBlock == null) { + return; + } + + // Find the ConditionalBlock that branches on the while condition. + ConditionalBlock cblock = findConditionalSuccessor(condBlock); + if (cblock == null) { + // condition often lives in ExceptionBlocks; try walking up preds and retry + Block peeled = peelExceptionBlocksToPred(condBlock, 50); + if (peeled != null) { + cblock = findConditionalSuccessor(peeled); + condBlock = peeled; + } + } + if (cblock == null) { + return; + } + + Block loopBodyEntryBlock = cblock.getThenSuccessor(); + + // Node for the extraction call tree (it.next()/q.poll()/s.pop()). + Node elementNode = anyNodeForTree(extraction.extractionCall); + if (elementNode == null) { + return; + } + + // 4) Record a "pendingCfg" potentially fulfilling loop. + // + // We store: + // - collectionTree (resources / q / s) + // - collectionElementTree (it.next() / q.poll() / s.pop()) + // - condition tree (the while condition) + // + // We leave CFG fields null and mark pendingCfg=true. + RLCCalledMethodsAnnotatedTypeFactory.addPotentiallyFulfillingLoop( + header.collectionTree, + extraction.extractionCall, // IMPORTANT: element is the extraction call tree + condNoParens, + /* loopBodyEntryBlock */ loopBodyEntryBlock, + /* loopUpdateBlock */ null, + /* conditionalBlock */ cblock, + /* collectionElementNode */ elementNode, + /* pendingCfg */ true); + } + + private @Nullable Block firstBlockForTree(Tree t) { + Set nodes = atypeFactory.getNodesForTree(t); + if (nodes == null || nodes.isEmpty()) return null; + for (Node n : nodes) { + Block b = n.getBlock(); + if (b != null) return b; + } + return null; + } + + private @Nullable Node anyNodeForTree(Tree t) { + Set nodes = atypeFactory.getNodesForTree(t); + if (nodes == null || nodes.isEmpty()) return null; + return nodes.iterator().next(); + } + + private @Nullable ConditionalBlock findConditionalSuccessor(Block b) { + for (Block succ : b.getSuccessors()) { + if (succ instanceof ConditionalBlock) return (ConditionalBlock) succ; + } + if (b instanceof SingleSuccessorBlock) { + Block succ = ((SingleSuccessorBlock) b).getSuccessor(); + if (succ instanceof ConditionalBlock) return (ConditionalBlock) succ; + } + return null; + } + + private @Nullable Block peelExceptionBlocksToPred(Block b, int budget) { + Block cur = b; + while (budget-- > 0 && cur instanceof ExceptionBlock) { + Set preds = cur.getPredecessors(); + if (preds.size() != 1) break; + Block p = preds.iterator().next(); + if (p == null) break; + cur = p; + } + return cur; + } + + private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { + // Case A: while (it.hasNext()) + if (cond instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) cond; + if (TreeUtils.isHasNextCall(mit)) { + ExpressionTree recv = receiverOfInvocation(mit); + Name itName = getNameFromExpressionTree(recv); + if (itName == null) { + return null; + } + ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); + if (colExpr == null) { + return null; + } + Name colName = getNameFromExpressionTree(colExpr); + return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); + } + } + + // Case B1: while (!c.isEmpty()) + if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { + ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); + WhileHeaderMatch m = matchNonEmptyFromExpr(inner); + if (m != null) return m; + } + + // Case B2: while (c.size() > 0) or while (0 < c.size()) + if (cond instanceof BinaryTree) { + WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); + if (m != null) return m; + } + + return null; + } + + private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { + if (!(inner instanceof MethodInvocationTree)) { + return null; + } + MethodInvocationTree mit = (MethodInvocationTree) inner; + if (!isIsEmptyCall(mit)) { + return null; + } + ExpressionTree recv = receiverOfInvocation(mit); + if (recv == null) return null; + + Name varName = getNameFromExpressionTree(recv); + if (varName == null) return null; + + Element recvElt = TreeUtils.elementFromTree(recv); + if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + return null; + } + + ExpressionTree colTree = collectionTreeFromExpression(recv); + if (colTree == null) return null; + + return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + } + + private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree bt) { + Tree.Kind k = bt.getKind(); + if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { + return null; + } + + ExpressionTree left = TreeUtils.withoutParens(bt.getLeftOperand()); + ExpressionTree right = TreeUtils.withoutParens(bt.getRightOperand()); + + // Normalize: accept "c.size() > 0" or "0 < c.size()" + MethodInvocationTree sizeCall = null; + LiteralTree zero = null; + + if (k == Tree.Kind.GREATER_THAN) { + // left must be size(), right must be 0 + if (left instanceof MethodInvocationTree && right instanceof LiteralTree) { + sizeCall = (MethodInvocationTree) left; + zero = (LiteralTree) right; + } + } else { // LESS_THAN + // left must be 0, right must be size() + if (left instanceof LiteralTree && right instanceof MethodInvocationTree) { + zero = (LiteralTree) left; + sizeCall = (MethodInvocationTree) right; + } + } + + if (sizeCall == null + || !(zero.getValue() instanceof Integer) + || (Integer) zero.getValue() != 0) { + return null; + } + if (!TreeUtils.isSizeAccess(sizeCall)) { + return null; + } + + ExpressionTree recv = receiverOfInvocation(sizeCall); + if (recv == null) return null; + + Name varName = getNameFromExpressionTree(recv); + if (varName == null) return null; + + Element recvElt = TreeUtils.elementFromTree(recv); + if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + return null; + } + + ExpressionTree colTree = collectionTreeFromExpression(recv); + if (colTree == null) return null; + + return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + } + + private boolean isIsEmptyCall(MethodInvocationTree mit) { + ExpressionTree sel = mit.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) { + return false; + } + MemberSelectTree ms = (MemberSelectTree) sel; + return ms.getIdentifier().contentEquals("isEmpty") && mit.getArguments().isEmpty(); + } + + private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree mit) { + ExpressionTree sel = mit.getMethodSelect(); + if (sel instanceof MemberSelectTree) { + return ((MemberSelectTree) sel).getExpression(); + } + return null; + } + + /** Recover "col" from: Iterator it = col.iterator(); while (it.hasNext()) { ... } */ + private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver(ExpressionTree itExpr) { + if (itExpr == null) return null; + + Element itElt = TreeUtils.elementFromTree(itExpr); + if (!(itElt instanceof VariableElement)) return null; + + // Only recover from local variable declaration with initializer "col.iterator()" + if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { + return null; + } + + Tree decl = atypeFactory.declarationFromElement(itElt); + if (!(decl instanceof VariableTree)) return null; + + ExpressionTree init = ((VariableTree) decl).getInitializer(); + if (!(init instanceof MethodInvocationTree)) return null; + + MethodInvocationTree initCall = (MethodInvocationTree) init; + ExpressionTree sel = initCall.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) return null; + + MemberSelectTree ms = (MemberSelectTree) sel; + if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { + return null; + } + + ExpressionTree colExpr = ms.getExpression(); + Element colElt = TreeUtils.elementFromTree(colExpr); + if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { + return null; + } + + return collectionTreeFromExpression(colExpr); + } + + private static final class BodyExtraction { + final MethodInvocationTree extractionCall; // it.next()/q.poll()/s.pop() + + BodyExtraction(MethodInvocationTree extractionCall) { + this.extractionCall = extractionCall; + } + } + + /** + * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns null + * (conservative/sound). + * + *

Also rejects break/return and writes to iterator/collection vars. + */ + private @Nullable BodyExtraction findSingleExtractionInWhileBody( + List statements, + Name headerVar, + @Nullable Name collectionVarName, + Set allowedExtractMethods) { + + AtomicBoolean illegal = new AtomicBoolean(false); + final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; + final int[] extractionCount = new int[] {0}; + + TreeScanner scanner = + new TreeScanner() { + + private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { + Name assigned = getNameFromExpressionTree(lhs); + if (assigned != null) { + if (assigned == headerVar) illegal.set(true); + if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); + } + } + + private void recordExtractionIfAny(ExpressionTree expr) { + expr = TreeUtils.withoutParens(expr); + if (!(expr instanceof MethodInvocationTree)) return; + + MethodInvocationTree mit = (MethodInvocationTree) expr; + if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) return; + + extractionCount[0]++; + if (extractionCount[0] > 1) { + illegal.set(true); + return; + } + extraction[0] = mit; + } + + @Override + public Void visitBreak(BreakTree node, Void p) { + illegal.set(true); + return super.visitBreak(node, p); + } + + @Override + public Void visitReturn(ReturnTree node, Void p) { + illegal.set(true); + return super.visitReturn(node, p); + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { + markWriteIfTargetsHeaderOrCollection(node.getVariable()); + return super.visitCompoundAssignment(node, p); + } + + @Override + public Void visitAssignment(AssignmentTree node, Void p) { + markWriteIfTargetsHeaderOrCollection(node.getVariable()); + recordExtractionIfAny(node.getExpression()); // r = it.next() + return super.visitAssignment(node, p); + } + + @Override + public Void visitVariable(VariableTree vt, Void p) { + ExpressionTree init = vt.getInitializer(); + if (init != null) recordExtractionIfAny(init); // T r = it.next() + return super.visitVariable(vt, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + // Direct use: it.next().close() => receiver is it.next() + ExpressionTree sel = mit.getMethodSelect(); + if (sel instanceof MemberSelectTree) { + ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); + recordExtractionIfAny(recv); + } + return super.visitMethodInvocation(mit, p); + } + }; + + for (StatementTree st : statements) { + scanner.scan(st, null); + if (illegal.get()) break; + } + + if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { + return null; + } + return new BodyExtraction(extraction[0]); + } + + private boolean isExtractionCallOnHeaderVar( + MethodInvocationTree mit, Name headerVar, Set allowedExtractMethods) { + ExpressionTree sel = mit.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) { + return false; + } + MemberSelectTree ms = (MemberSelectTree) sel; + String methodName = ms.getIdentifier().toString(); + if (!allowedExtractMethods.contains(methodName)) { + return false; + } + if (!mit.getArguments().isEmpty()) { + return false; + } + Name recv = getNameFromExpressionTree(ms.getExpression()); + return recv != null && recv == headerVar; + } + /** * Marks the for-loop if it potentially fulfills collection obligations of a collection. * @@ -619,9 +1090,14 @@ protected Name getNameFromExpressionTree(ExpressionTree expr) { case IDENTIFIER: return ((IdentifierTree) expr).getName(); case MEMBER_SELECT: - Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); - if (elt.getKind() == ElementKind.METHOD || elt.getKind() == ElementKind.FIELD) { - return getNameFromExpressionTree(((MemberSelectTree) expr).getExpression()); + MemberSelectTree mst = (MemberSelectTree) expr; + Element elt = TreeUtils.elementFromUse(mst); + if (elt.getKind() == ElementKind.FIELD) { + // this.files ==> "files" (NOT "this") + return mst.getIdentifier(); + } else if (elt.getKind() == ElementKind.METHOD) { + // resources.size ==> "resources" + return getNameFromExpressionTree(mst.getExpression()); } else { return null; } @@ -665,9 +1141,12 @@ protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { case IDENTIFIER: return expr; case MEMBER_SELECT: - Element elt = TreeUtils.elementFromUse((MemberSelectTree) expr); + MemberSelectTree mst = (MemberSelectTree) expr; + Element elt = TreeUtils.elementFromUse(mst); if (elt.getKind() == ElementKind.METHOD) { return ((MemberSelectTree) expr).getExpression(); + } else if (elt.getKind() == ElementKind.FIELD) { + return expr; } else { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 1e63d4bc3ef8..c46ec13e4067 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -1902,6 +1902,13 @@ private void checkReassignmentToOwningCollectionField( CollectionOwnershipType lhsCoType = coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); if (lhsCoType == null) { + if (TreeUtils.isConstructor(enclosingMethodTree)) { + // If its in the constructor, it may be the first assignment to the field. + // TODO: after PR #7050 is merged, use that logic here to determine if first assignment. + return; + } + // TODO: instead of throwing an exception here, we should probably treat it as Owning + // collection and issue an error below to be sound. throw new BugInCF( "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); } else if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom @@ -3502,6 +3509,8 @@ public void analyzeObligationFulfillingLoop( visited.add(loopBodyEntry); Set calledMethodsInLoop = null; + Set loopRegion = computeLoopRegion(loopBodyEntryBlock, loopUpdateBlock); + // main loop: propagate obligations block-by-block while (!worklist.isEmpty()) { BlockWithObligations current = worklist.remove(); @@ -3518,6 +3527,10 @@ public void analyzeObligationFulfillingLoop( @SuppressWarnings("interning:not.interned") boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock; + boolean staysInLoop = loopRegion.contains(successorAndExceptionType.first); + if (!isLastBlockOfBody && !staysInLoop) { + return; + } if (isLastBlockOfBody) { Set calledMethodsAfterBlock = analyzeTypeOfCollectionElement( @@ -3556,6 +3569,73 @@ public void analyzeObligationFulfillingLoop( } } + private Set computeLoopRegion(Block entry, Block update) { + // Forward reachability (using getSuccessorsExceptIgnoredExceptions), + // and build the reverse graph *for the same filtered edges*. + Set forward = new HashSet<>(); + Map> allowedPreds = new HashMap<>(); + + Deque wl = new ArrayDeque<>(); + wl.add(entry); + forward.add(entry); + + while (!wl.isEmpty()) { + Block b = wl.removeFirst(); + + for (IPair succ : getSuccessorsExceptIgnoredExceptions(b)) { + Block s = succ.first; + if (s == null) { + continue; + } + + // Record filtered predecessor relation for backward traversal. + allowedPreds.computeIfAbsent(s, k -> new LinkedHashSet<>()).add(b); + + // We allow edges *to* update, but we do not expand beyond update. + if (s == update) { + continue; + } + + if (forward.add(s)) { + wl.addLast(s); + } + } + } + + // If update isn't even reachable from entry under the filtered edge relation, + // region is empty (nothing meaningful to certify). + if (!forward.contains(update) && !allowedPreds.containsKey(update)) { + return Collections.emptySet(); + } + + // Backward reachability to update, but ONLY using the filtered reverse edges we built. + Set forwardPlus = new HashSet<>(forward); + forwardPlus.add(update); + + Set canReachUpdate = new HashSet<>(); + Deque bwl = new ArrayDeque<>(); + bwl.add(update); + canReachUpdate.add(update); + + while (!bwl.isEmpty()) { + Block b = bwl.removeFirst(); + + for (Block pred : allowedPreds.getOrDefault(b, Collections.emptySet())) { + if (!forwardPlus.contains(pred)) { + continue; + } + if (canReachUpdate.add(pred)) { + bwl.addLast(pred); + } + } + } + + // Region = forward ∩ canReachUpdate, excluding update itself. + forward.retainAll(canReachUpdate); + forward.remove(update); + return forward; + } + /** * Checks the CalledMethods store after the given block and determines the CalledMethods type of * the given tree (which corresponds to the collection element iterated over) and returns the @@ -3614,6 +3694,7 @@ private Set analyzeTypeOfCollectionElement( } // add the called methods of possible aliases of the collection element + boolean hasAnyLiveAlias = false; for (ResourceAlias alias : collectionElementObligation.resourceAliases) { AccumulationValue cmValOfAlias = store.getValue(alias.reference); if (cmValOfAlias == null) { @@ -3621,6 +3702,7 @@ private Set analyzeTypeOfCollectionElement( } List calledMethods = getCalledMethods(cmValOfAlias); if (calledMethods != null) { + hasAnyLiveAlias = true; if (calledMethodsAfterThisBlock == null) { calledMethodsAfterThisBlock = new HashSet<>(calledMethods); } else { @@ -3628,6 +3710,10 @@ private Set analyzeTypeOfCollectionElement( } } } + if (!hasAnyLiveAlias) { + // all aliases are dead; called methods is bottom + return null; + } return calledMethodsAfterThisBlock; } @@ -3655,7 +3741,7 @@ private List getCalledMethods(AccumulationValue cmVal) { } } } - return new ArrayList<>(); + return null; } /** diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 91e0a3daf930..cf11cd97edb4 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -11,10 +11,14 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.Set; import javax.lang.model.element.AnnotationMirror; @@ -155,6 +159,27 @@ public static void addPotentiallyFulfillingLoop( collectionEltNode)); } + public static void addPotentiallyFulfillingLoop( + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree condition, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, + Node collectionEltNode, + boolean pendingCfg) { + potentiallyFulfillingLoops.add( + new PotentiallyFulfillingLoop( + collectionTree, + collectionElementTree, + condition, + loopBodyEntryBlock, + loopUpdateBlock, + loopConditionalBlock, + collectionEltNode, + pendingCfg)); + } + /** * Returns the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. * @@ -689,7 +714,7 @@ public static class PotentiallyFulfillingLoop { public final Block loopBodyEntryBlock; /** cfg {@code Block} containing the loop update. */ - public final Block loopUpdateBlock; + public Block loopUpdateBlock; /** cfg conditional {@link Block} following loop condition. */ public final ConditionalBlock loopConditionalBlock; @@ -697,6 +722,8 @@ public static class PotentiallyFulfillingLoop { /** cfg {@code Node} for the collection element iterated over. */ public final Node collectionElementNode; + public boolean pendingCfg = false; + /** * Constructs a new {@code PotentiallyFulfillingLoop} * @@ -726,6 +753,34 @@ public PotentiallyFulfillingLoop( this.collectionElementNode = collectionEltNode; } + public PotentiallyFulfillingLoop( + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree condition, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, + Node collectionEltNode, + boolean pendingCfg) { + this.collectionTree = collectionTree; + this.collectionElementTree = collectionElementTree; + this.condition = condition; + this.calledMethods = new HashSet<>(); + this.loopBodyEntryBlock = loopBodyEntryBlock; + this.loopUpdateBlock = loopUpdateBlock; + this.loopConditionalBlock = loopConditionalBlock; + this.collectionElementNode = collectionEltNode; + this.pendingCfg = pendingCfg; + } + + public boolean isResolved() { + return !pendingCfg + && loopBodyEntryBlock != null + && loopUpdateBlock != null + && loopConditionalBlock != null + && collectionElementNode != null; + } + /** * Add methods that are guaranteed to be invoked on every element of the collection the loop * iterates over. @@ -762,6 +817,8 @@ public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); + resolvePendingWhileLoopUpdateBlocks(cfg); + // traverse the cfg to find enhanced-for-loops over collections and perform a // loop-body-analysis. // since this runs before the consistency analysis, we unfortunately have to traverse the cfg @@ -787,4 +844,152 @@ public void postAnalyze(ControlFlowGraph cfg) { super.postAnalyze(cfg); } + + public void resolvePendingWhileLoopUpdateBlocks(ControlFlowGraph cfg) { + Block entry = cfg.getEntryBlock(); + Set blocks = reachableFrom(entry, 500); + + Map> dom = computeDominators(entry, blocks); + List backEdges = findBackEdges(blocks, dom); + + for (PotentiallyFulfillingLoop loop : getPotentiallyFulfillingLoops()) { + if (!loop.pendingCfg) continue; + if (loop.loopConditionalBlock == null || loop.loopBodyEntryBlock == null) continue; + + Block header = chooseHeaderForLoop(loop, backEdges, blocks); + if (header != null) { + loop.loopUpdateBlock = header; // back-edge target = loop "header" + loop.pendingCfg = false; + } + } + } + + private @Nullable Block chooseHeaderForLoop( + PotentiallyFulfillingLoop loop, List backEdges, Set blocks) { + + Block bodyEntry = loop.loopBodyEntryBlock; + Block condBlock = loop.loopConditionalBlock; + + Block bestHeader = null; + int bestSize = Integer.MAX_VALUE; + + for (Edge e : backEdges) { + // e.v is header candidate (dominates e.u) + Set natural = naturalLoop(e.u, e.v, blocks, 100000); + + // Must contain this while’s body entry and conditional block + if (!natural.contains(bodyEntry)) continue; + if (!natural.contains(condBlock)) continue; + + // Prefer tightest loop (helps nested-loop disambiguation) + if (natural.size() < bestSize) { + bestSize = natural.size(); + bestHeader = e.v; + } + } + + return bestHeader; + } + + // ----- Dominators / natural-loop machinery ----- + + private static final class Edge { + final Block u, v; // u -> v + + Edge(Block u, Block v) { + this.u = u; + this.v = v; + } + } + + private Set reachableFrom(Block entry, int budget) { + Set seen = new HashSet<>(); + ArrayDeque q = new ArrayDeque<>(); + q.add(entry); + seen.add(entry); + + while (!q.isEmpty() && budget-- > 0) { + Block b = q.remove(); + for (Block s : b.getSuccessors()) { + if (s != null && seen.add(s)) { + q.add(s); + } + } + } + return seen; + } + + private Map> computeDominators(Block entry, Set blocks) { + Map> dom = new HashMap<>(); + + for (Block b : blocks) { + if (b == entry) { + dom.put(b, new HashSet<>(Collections.singleton(entry))); + } else { + dom.put(b, new HashSet<>(blocks)); // TOP + } + } + + boolean changed; + do { + changed = false; + for (Block b : blocks) { + if (b == entry) continue; + + Set newDom = null; + for (Block p : b.getPredecessors()) { + if (p == null || !blocks.contains(p)) continue; + Set pDom = dom.get(p); + if (pDom == null) continue; + if (newDom == null) newDom = new HashSet<>(pDom); + else newDom.retainAll(pDom); + } + + if (newDom == null) newDom = new HashSet<>(); + newDom.add(b); + + if (!newDom.equals(dom.get(b))) { + dom.put(b, newDom); + changed = true; + } + } + } while (changed); + + return dom; + } + + private List findBackEdges(Set blocks, Map> dom) { + List backs = new ArrayList<>(); + for (Block u : blocks) { + for (Block v : u.getSuccessors()) { + if (v == null || !blocks.contains(v)) continue; + Set domU = dom.get(u); + if (domU != null && domU.contains(v)) { + // v dominates u => back edge u -> v + backs.add(new Edge(u, v)); + } + } + } + return backs; + } + + private Set naturalLoop(Block u, Block v, Set blocks, int budget) { + // Standard natural-loop: start from backedge u->v + Set loop = new HashSet<>(); + ArrayDeque stack = new ArrayDeque<>(); + + loop.add(v); + if (loop.add(u)) stack.push(u); + + while (!stack.isEmpty() && budget-- > 0) { + Block x = stack.pop(); + for (Block p : x.getPredecessors()) { + if (p == null || !blocks.contains(p)) continue; + if (loop.add(p) && p != v) { + stack.push(p); + } + } + } + return loop; + } } diff --git a/checker/tests/resourceleak-collections/FinallyBug.java b/checker/tests/resourceleak-collections/FinallyBug.java new file mode 100644 index 000000000000..adf4e20d8a50 --- /dev/null +++ b/checker/tests/resourceleak-collections/FinallyBug.java @@ -0,0 +1,28 @@ +import java.util.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class FinallyBug { + void f(@OwningCollection List col) { + try { + for (Resource r : col) { + r.flush(); + r.close(); + } + } finally { + col.clear(); + } + } + + void f2(@OwningCollection List col) { + try { + for (Resource r : col) { + r.flush(); + r.close(); + } + } finally { + + } + col.clear(); + } +} diff --git a/checker/tests/resourceleak-collections/GraphFoundationCrash.java b/checker/tests/resourceleak-collections/GraphFoundationCrash.java new file mode 100644 index 000000000000..43b17bddabad --- /dev/null +++ b/checker/tests/resourceleak-collections/GraphFoundationCrash.java @@ -0,0 +1,13 @@ +import java.util.ArrayList; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class CrashRepro { + + // :: error: unfulfilled.field.obligations + private ArrayList pool; + + CrashRepro(int maxSize) { + this.pool = new ArrayList<>(maxSize); // should trigger the crash path + } +} diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 077ce0d18fe4..0e8316759f38 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -97,6 +97,7 @@ void nullableElementWithCheck(@OwningCollection List resources) { r.flush(); } } + checkArgIsOCWO(resources); } void nullableElementHelper(@OwningCollection List resources) { diff --git a/checker/tests/resourceleak-collections/MissingAnnotations.java b/checker/tests/resourceleak-collections/MissingAnnotations.java new file mode 100644 index 000000000000..6a5601d8a256 --- /dev/null +++ b/checker/tests/resourceleak-collections/MissingAnnotations.java @@ -0,0 +1,53 @@ +import java.io.*; +import java.nio.channels.*; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class SetTest { + // :: error: unfulfilled.field.obligations + Set resSet = new HashSet<>(); + + private InputStream[] responseSequenceBinary(List fileNames) throws IOException { + List response = new ArrayList<>(); + for (var fileName : fileNames) { + // :: error: unfulfilled.collection.obligations + response.add(new FileInputStream(fileName)); + } + // :: error: method.invocation + return response.toArray(new InputStream[0]); + } +} + +@InheritableMustCall("close") +class DestructorWithThis { + private final Queue files; + + DestructorWithThis(int size) { + this.files = new ConcurrentLinkedQueue(); + } + + @CreatesMustCallFor("this") + public void release(RandomAccessFile file) { + this.files.add(file); + } + + @CollectionFieldDestructor("this.files") + public void close() throws IOException { + try { + while (!this.files.isEmpty()) { + RandomAccessFile pooledFile = this.files.poll(); + if (pooledFile != null) { + try { + pooledFile.close(); + } catch (IOException e) { + } + } + } + } finally { + this.files.clear(); + } + } +} diff --git a/checker/tests/resourceleak-collections/Test.java b/checker/tests/resourceleak-collections/Test.java new file mode 100644 index 000000000000..daf840f8bf2f --- /dev/null +++ b/checker/tests/resourceleak-collections/Test.java @@ -0,0 +1,83 @@ +import java.io.IOException; +import java.util.*; +import java.util.Iterator; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class CollectionFieldDestructorExceptionPathTest { + + @InheritableMustCall("close") + static class Resource2 { + void close() throws IOException {} + } + + @InheritableMustCall("shutdownConnections") + static class Worker { + + final @OwningCollection List tcpConnections = new ArrayList<>(); + + @CreatesMustCallFor("this") + void add(@Owning Resource2 resource) { + tcpConnections.add(resource); + } + + @CollectionFieldDestructor("this.tcpConnections") + // ::error: contracts.postcondition + void shutdownConnections() throws IOException { + Iterator it = tcpConnections.iterator(); + while (it.hasNext()) { + Resource2 r = it.next(); + r.close(); + } + } + } + + @InheritableMustCall("shutdownConnections") + static class Worker2 { + + final @OwningCollection List tcpConnections = new ArrayList<>(); + + @CreatesMustCallFor("this") + void add(@Owning Resource2 resource) { + tcpConnections.add(resource); + } + + @CollectionFieldDestructor("this.tcpConnections") + void shutdownConnections() throws IOException { + Iterator it = tcpConnections.iterator(); + while (it.hasNext()) { + Resource2 r = it.next(); + try { + r.close(); + } catch (IOException e) { + } + } + } + } + + // :: error: illegal.type.annotation + static void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} + + void client() throws Exception { + Worker w = new Worker(); + w.add(new Resource2()); + w.shutdownConnections(); + } + + void client2() { + Worker w = new Worker(); + w.add(new Resource2()); + try { + w.shutdownConnections(); + } catch (IOException e) { + } + } + + void client3() { + Resource2 r = new Resource2(); + try { + r.close(); + } catch (IOException e) { + } + } +} diff --git a/checker/tests/resourceleak-collections/WhileLoopDestructor.java b/checker/tests/resourceleak-collections/WhileLoopDestructor.java new file mode 100644 index 000000000000..4675fdc56a84 --- /dev/null +++ b/checker/tests/resourceleak-collections/WhileLoopDestructor.java @@ -0,0 +1,143 @@ +import java.io.*; +import java.io.Closeable; +import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.Stack; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class LoopBodyAnalysisWhileTests { + + // ------------------------- + // WHILE LOOP: Iterator.hasNext/next + // ------------------------- + void whileIteratorFull(@OwningCollection Collection resources) { + Iterator it = resources.iterator(); + while (it.hasNext()) { + Resource r = it.next(); + r.close(); + r.flush(); + } + checkArgIsOCWO(resources); + } + + void whileIteratorFullNullCheck(@OwningCollection Collection resources) { + Iterator it = resources.iterator(); + while (it.hasNext()) { + Resource r = it.next(); + if (r != null) { + r.close(); + r.flush(); + } + } + checkArgIsOCWO(resources); + } + + // :: error: unfulfilled.collection.obligations + void whileIteratorFullEarlyBreak(@OwningCollection Collection resources) { + Iterator it = resources.iterator(); + while (it.hasNext()) { + Resource r = it.next(); + r.close(); + if (resources.isEmpty()) { + break; + } + r.flush(); + } + // :: error: argument + checkArgIsOCWO(resources); + } + + // ------------------------- + // WHILE LOOP: Queue.isEmpty/poll + // ------------------------- + void whileQueuePollFull(@OwningCollection Queue resources) { + while (!resources.isEmpty()) { + Resource r = resources.poll(); + if (r != null) { + r.close(); + r.flush(); + } + } + checkArgIsOCWO(resources); + } + + // ------------------------- + // WHILE LOOP: Stack.isEmpty/pop (FULL) + // ------------------------- + void whileStackPopFull(@OwningCollection Stack resources) { + while (!resources.isEmpty()) { + Resource r = resources.pop(); + r.close(); + r.flush(); + } + checkArgIsOCWO(resources); + } + + void whileStackPopFullWithSize(@OwningCollection Stack resources) { + while (resources.size() > 0) { + Resource r = resources.pop(); + r.close(); + r.flush(); + } + checkArgIsOCWO(resources); + } + + // ------------------------- + // NEGATIVE: Iterator while-loop missing flush + // ------------------------- + + // :: error: unfulfilled.collection.obligations + void whileIteratorPartialShouldError(@OwningCollection List resources) { + Iterator it = resources.iterator(); + while (it.hasNext()) { + Resource r = it.next(); + r.close(); + // missing flush + } + // :: error: argument + checkArgIsOCWO(resources); + } + + void whileIteratorPartialShouldError2(@OwningCollection List resources) + throws IOException { + Iterator it = resources.iterator(); + while (it.hasNext()) { + FileInputStream r = it.next(); + try { + r.close(); + } catch (IOException e) { + } + } + checkArgIsOCWO2(resources); + } + + // Helper used by the tests (same as your for-loop file). + // :: error: illegal.type.annotation + void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} + + // :: error: illegal.type.annotation + void checkArgIsOCWO2(@OwningCollectionWithoutObligation Iterable arg) {} +} + +abstract class RLCCollections { + + abstract Closeable alloc(); + + void foo() throws Exception { + @OwningCollection Collection resources = new ArrayList(); + resources.add(alloc()); + + for (var r : resources) { + try { + r.close(); + } catch (IOException e) { + } + } + } +} diff --git a/checker/tests/resourceleak/TempTest.java b/checker/tests/resourceleak/TempTest.java new file mode 100644 index 000000000000..e2590574894b --- /dev/null +++ b/checker/tests/resourceleak/TempTest.java @@ -0,0 +1,21 @@ +import java.io.IOException; +import java.util.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class CollectionFieldDestructorExceptionPathTest { + + @InheritableMustCall("close") + static class Resource2 { + void close() throws IOException {} + } + + void client3() { + Resource2 r = new Resource2(); + try { + r.close(); + } catch (IOException e) { + } + System.out.println(); + } +} From 3b5490e1530871191963a186faaecff31bc7a99f Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 26 Feb 2026 12:41:47 -0800 Subject: [PATCH 345/374] Allow mutation of not owning collection if inserted element is not owning. --- .../CollectionOwnershipVisitor.java | 153 +++++++++++++ .../collectionownership/messages.properties | 2 + .../MustCallConsistencyAnalyzer.java | 35 +++ .../NotOwningCollectionMutationTest.java | 204 ++++++++++++++++++ .../OwningCollectionFieldTyping.java | 3 +- checker/tests/resourceleak/TempTest.java | 21 -- 6 files changed, 396 insertions(+), 22 deletions(-) create mode 100644 checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java delete mode 100644 checker/tests/resourceleak/TempTest.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index c872f51b652f..1bdeee108aa4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -1,6 +1,8 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.VariableTree; import java.util.List; import javax.lang.model.element.AnnotationMirror; @@ -8,18 +10,26 @@ 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; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** * The visitor for the Collection Ownership Checker. This visitor is similar to BaseTypeVisitor, but @@ -48,6 +58,149 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return super.visitAnnotation(tree, p); } + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + // Enforce additional policy for collection mutators. + enforceCreatesCollectionObligationPolicy(tree); + return super.visitMethodInvocation(tree, p); + } + + /** + * Enforces the "mutations on non-owning collections" policy for methods annotated + * {@code @CreatesCollectionObligation}. + * + *

Strategy: (A) Owning receiver (OC or OCWO): the call is a transfer point. It should not be + * used to insert a definitely-non-owning element. (B) NotOwning receiver (NOC): allow mutation + * ONLY if the inserted thing is definitely non-owning. This prevents "smuggling" an owning / + * obligation-carrying element into a non-owning collection. + * + *

Note: we intentionally do not add an index property to @CreatesCollectionObligation yet. We + * use a heuristic: the "inserted thing" is the last argument at the call site. TODO: Maybe later + * remove this heuristic and require and index on the CreatesCollectionObligation annotation to + * determine the inserted element's index. + */ + private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) { + ExecutableElement methodElt = TreeUtils.elementFromUse(tree); + boolean isCreates = + atypeFactory.getDeclAnnotation( + methodElt, + org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation + .class) + != null; + if (!isCreates) { + return; + } + ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree); + if (receiverTree == null) { + // Static call or no explicit receiver; not a collection mutator on an instance receiver. + return; + } + if (!atypeFactory.isResourceCollection(receiverTree)) { + return; + } + if (tree.getArguments().isEmpty()) { + // No "inserted thing" to validate. + return; + } + CollectionOwnershipStore storeBefore = atypeFactory.getStoreBefore(tree); + CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType recvType = + getCoTypeAtTree(receiverTree, storeBefore); + if (recvType == null) { + return; + } + // Heuristic: inserted resource is at the last index of call + ExpressionTree insertedTree = tree.getArguments().get(tree.getArguments().size() - 1); + RLCCalledMethodsAnnotatedTypeFactory rlAtf = + ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(atypeFactory); + boolean insertedDefinitelyNonOwning = isInsertedThingDefinitelyNonOwning(insertedTree, rlAtf); + String methodName = methodElt.getSimpleName().toString(); + switch (recvType) { + case NotOwningCollection: + if (!insertedDefinitelyNonOwning) { + checker.reportError( + insertedTree, + "illegal.collection.mutator.owning.insert.into.notowning", + methodName, + TreeUtils.toStringTruncated(insertedTree, 60)); + } + break; + case OwningCollection: + case OwningCollectionWithoutObligation: + // disallow inserting something that is definitely non-owning into an owning collection. + if (insertedDefinitelyNonOwning) { + checker.reportError( + insertedTree, + "illegal.collection.mutator.nonowning.insert.into.owning", + methodName, + TreeUtils.toStringTruncated(insertedTree, 60)); + } + break; + default: + // bottom : ignore + } + } + + /** Gets the flow-sensitive collection-ownership qualifier for {@code expr}, if available. */ + private CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType getCoTypeAtTree( + ExpressionTree expr, CollectionOwnershipStore storeBefore) { + if (storeBefore != null) { + JavaExpression je = JavaExpression.fromTree(expr); + CFValue v = storeBefore.getValue(je); + if (v != null) { + return atypeFactory.getCoType(v.getAnnotations()); + } + } + return null; + } + + /** + * Returns true iff the "inserted thing" is definitely non-owning at this call site. + * + *

For non-collection values: - annotation-first: explicit @NotOwning on the + * expression/return/element, if present - fallback: empty @MustCall (no obligation exists) + * + *

For resource-collection values (e.g., addAll/putAll where the "inserted thing" is a + * collection): - annotation-first: the argument must be @NotOwningCollection at the call site - + * we intentionally avoid inspecting element obligations here without extra machinery. + */ + private boolean isInsertedThingDefinitelyNonOwning( + ExpressionTree insertedTree, RLCCalledMethodsAnnotatedTypeFactory rlAtf) { + + // If the inserted thing is itself a resource collection (bulk ops e.g. addAll) + if (atypeFactory.isResourceCollection(insertedTree)) { + return false; + } + + // annotation-first: explicit @NotOwning + // 1) On the element/variable symbol (locals/params/fields) + Element e = TreeUtils.elementFromTree(insertedTree); + if (e != null && e.getAnnotation(NotOwning.class) != null) { + return true; + } + // 2) On the return type of a method invocation + if (insertedTree instanceof MethodInvocationTree) { + ExecutableElement callee = TreeUtils.elementFromUse((MethodInvocationTree) insertedTree); + if (rlAtf.hasNotOwning(callee)) { + return true; + } + } + // 3) On the type at the tree (covers return-type annotations in many cases) + MustCallAnnotatedTypeFactory mcAtf = rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotatedTypeMirror mcType = mcAtf.getAnnotatedType(insertedTree); + if (mcType != null && mcType.hasPrimaryAnnotation(NotOwning.class)) { + return true; + } + + // fallback: empty must-call => no obligation to "smuggle" --- + TypeMirror tm = TreeUtils.typeOf(insertedTree); + TypeElement typeElt = TypesUtils.getTypeElement(tm); + if (typeElt == null) { + return false; + } + // If the type can never have a must-call obligation, treat as definitely safe. + return rlAtf.hasEmptyMustCallValue(typeElt); + } + /** * This method checks that the result type of a constructor is a supertype of the declared type on * the class, if one exists. diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index f5ed712d0849..29505661d818 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -3,3 +3,5 @@ unfulfilled.field.obligations=The field %s may not have all of its @MustCall %s transfer.owningcollection.field.ownership=Method invocation unsafely transfers the ownership away from field %s. An @OwningCollection field can never lose ownership. illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. +illegal.collection.mutator.owning.insert.into.notowning=Call to %s not allowed on a @NotOwningCollection receiver when inserted value may be owning: %s +illegal.collection.mutator.nonowning.insert.into.owning=Call to %s not allowed on an @OwningCollection receiver when inserted value is definitely @NotOwning: %s diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index c46ec13e4067..01d4f59bc527 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -844,12 +844,47 @@ private void addObligationsForCreatesCollectionObligationAnno( checkEnclosingMethodIsCreatesMustCallFor(receiverNode, enclosingMethodTree); } } + // consume the inserted elements obligation + consumeInsertedArgumentObligationIfSingleElementInsert(obligations, node); break; default: } } } + /** + * Models consumption of the inserted element's obligation by the receiver collection for + * {@code @CreatesCollectionObligation} calls on owning receivers. + * + *

Heuristic: "inserted thing" is the last argument. We only consume when the inserted thing is + * NOT itself a resource collection (i.e., avoid bulk ops). + */ + private void consumeInsertedArgumentObligationIfSingleElementInsert( + Set obligations, MethodInvocationNode node) { + List args = node.getArguments(); + if (args.isEmpty()) { + return; + } + Node inserted = removeCastsAndGetTmpVarIfPresent(args.get(args.size() - 1)); + if (inserted.getTree() != null && coAtf.isResourceCollection(inserted.getTree())) { + // Bulk op (addAll/putAll etc). + return; + } + + // Remove any tracked obligations for the inserted value from the caller context: + // responsibility is now represented by the collection obligation. + if (inserted instanceof LocalVariableNode) { + removeObligationsContainingVar(obligations, (LocalVariableNode) inserted); + return; + } + if (inserted.getTree() != null) { + Set toRemove = getObligationsForVar(obligations, inserted.getTree()); + for (Obligation o : toRemove) { + obligations.remove(o); + } + } + } + /** * Prevents leaking of resource collection fields owned by the enclosing class by simply * forbidding any access to it from anyone outside the immediate class. diff --git a/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java new file mode 100644 index 000000000000..81dc1a077c7c --- /dev/null +++ b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java @@ -0,0 +1,204 @@ +import java.io.IOException; +import java.util.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class NotOwningCollectionMutationTest { + + @InheritableMustCall("close") + static class R { + void close() throws IOException {} + } + + R owning() { + return new R(); + } + + @NotOwning + R notowning() { + // :: error: required.method.not.called + return new R(); + } + + // ------------------------------------------------------------ + // NotOwningCollection receiver + NotOwning element: OK + // ------------------------------------------------------------ + void ok_noc_add_notowning_param(@NotOwningCollection List l, @NotOwning R r) { + l.add(r); // ok + } + + void ok_noc_add_notowning_method(@NotOwningCollection List l) { + l.add(notowning()); // ok + } + + // ------------------------------------------------------------ + // NotOwningCollection receiver + owning element: error (smuggling) + // Close afterwards to avoid extra RLC "required.method.not.called". + // ------------------------------------------------------------ + void err_noc_add_owning_local_then_close(@NotOwningCollection List l) { + R r = new R(); + // :: error: illegal.collection.mutator.owning.insert.into.notowning + l.add(r); + try { + r.close(); + } catch (IOException e) { + // ignore + } + } + + void err_noc_add_owning_param_then_close(@NotOwningCollection List l, @Owning R r) { + // :: error: illegal.collection.mutator.owning.insert.into.notowning + l.add(r); + try { + r.close(); + } catch (IOException e) { + // ignore + } + } + + void err_noc_add_alias_of_owning_then_close(@NotOwningCollection List l) { + R r = new R(); + R r2 = r; + // :: error: illegal.collection.mutator.owning.insert.into.notowning + l.add(r2); + try { + r.close(); + } catch (IOException e) { + // ignore + } + } + + // ------------------------------------------------------------ + // NotOwningCollection receiver via move/alias: old name becomes NOC + // (assuming your assignment transfer sets RHS (old owner) to NOC) + // ------------------------------------------------------------ + void err_noc_receiver_after_move_then_close() { + @OwningCollection List owner = new ArrayList<>(); + List newOwner = owner; // after transfer: "owner" becomes @NotOwningCollection + R r = new R(); + // :: error: illegal.collection.mutator.owning.insert.into.notowning + owner.add(r); + try { + r.close(); + } catch (IOException e) { + // ignore + } + + for (R r2 : newOwner) { + try { + r2.close(); + } catch (Exception e) { + } + } + } + + // ------------------------------------------------------------ + // NotOwningCollection as a field: same rule (can add only non-owning) + // ------------------------------------------------------------ + + final @NotOwningCollection List cache = new ArrayList<>(); + + void ok_noc_field_add_notowning(@NotOwning R r) { + cache.add(r); // ok + } + + void err_noc_field_add_owning_then_close() { + R r = new R(); + // :: error: illegal.collection.mutator.owning.insert.into.notowning + cache.add(r); + try { + r.close(); + } catch (IOException e) { + // ignore + } + } + + // ------------------------------------------------------------ + // OwningCollection receiver + owning element: should create collection obligation. + // If you don't discharge via a certified loop, expect unfulfilled.collection.obligations. + // ------------------------------------------------------------ + + void err_oc_add_owning_local_no_dispose() { + @OwningCollection List l = new ArrayList<>(); + R r = new R(); + // :: error: unfulfilled.collection.obligations + l.add(r); + } + + void err_oc_add_owning_alias_no_dispose() { + @OwningCollection List l = new ArrayList<>(); + R r = new R(); + R r2 = r; + // :: error: unfulfilled.collection.obligations + l.add(r2); + } + + void err_oc_add_owning_newexpr_no_dispose() { + @OwningCollection List l = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + l.add(new R()); + } + + // Discharge via a certified loop (close is inside try/catch so no early-exit via exception). + void ok_oc_add_owning_then_dispose_loop() { + @OwningCollection List l = new ArrayList<>(); + R r = new R(); + l.add(r); + + for (R x : l) { + try { + x.close(); + } catch (IOException e) { + + } + } + } + + void ok_oc_add_two_then_dispose_loop() { + @OwningCollection List l = new ArrayList<>(); + R r1 = new R(); + R r2 = new R(); + l.add(r1); + l.add(r2); + + for (R x : l) { + try { + x.close(); + } catch (IOException e) { + // swallow + } + } + } + + // inserting @NotOwning elements into an owning collection: + void err_oc_insert_notowning(@OwningCollection List l, @NotOwning R r) { + // :: error: illegal.collection.mutator.nonowning.insert.into.owning + l.add(r); + close_collection(l); + } + + void close_collection(@OwningCollection List l) { + for (R r : l) { + try { + r.close(); + } catch (IOException e) { + } + } + } + + // ------------------------------------------------------------ + // addAll: TODO: Take care of the type errors in addAll via JDK stubs + // ------------------------------------------------------------ + + // void err_noc_receiver_addAll_noc_arg( + // @NotOwningCollection List dst, @NotOwningCollection List src) { + // // :: error: method.invocation + // dst.addAll(src); + // } + // + // void err_noc_receiver_addAll_oc_arg( + // @NotOwningCollection List dst, @OwningCollection List src) { + // // :: error: method.invocation + // dst.addAll(src); + // } +} diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java index 6629b0f18094..b75dc712db11 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java @@ -110,7 +110,8 @@ class OwningCollectionFieldTyping { void tryTransferringFieldOwnershipAssignment() { // try to steal ownership List ownershipStealer = ocField; - // :: error: method.invocation + // :: error: illegal.collection.mutator.owning.insert.into.notowning + // :: error: required.method.not.called ownershipStealer.add(new Resource()); } diff --git a/checker/tests/resourceleak/TempTest.java b/checker/tests/resourceleak/TempTest.java deleted file mode 100644 index e2590574894b..000000000000 --- a/checker/tests/resourceleak/TempTest.java +++ /dev/null @@ -1,21 +0,0 @@ -import java.io.IOException; -import java.util.*; -import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; - -class CollectionFieldDestructorExceptionPathTest { - - @InheritableMustCall("close") - static class Resource2 { - void close() throws IOException {} - } - - void client3() { - Resource2 r = new Resource2(); - try { - r.close(); - } catch (IOException e) { - } - System.out.println(); - } -} From aeb82cd6057e8c16cfc0607df729a2aa117d6c44 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Tue, 17 Mar 2026 15:41:42 -0700 Subject: [PATCH 346/374] bug fixes. --- .../CollectionOwnershipAnalysis.java | 2 +- .../CollectionOwnershipTransfer.java | 16 ++- .../CollectionOwnershipVisitor.java | 9 +- .../collectionownership/messages.properties | 1 + .../MustCallConsistencyAnalyzer.java | 124 +++++++++++++++++- .../RLCCalledMethodsAnnotatedTypeFactory.java | 2 +- .../resourceleak-collections/AnotherBug.java | 21 +++ .../tests/resourceleak-collections/Bug.java | 71 ++++++++++ .../tests/resourceleak-collections/Buggz.java | 74 +++++++++++ .../GraphFoundationCrash.java | 27 +++- .../InfiniteLoopGrowingCollection.java | 86 ++++++++++++ .../resourceleak-collections/MapGetType.java | 31 +++++ .../MissingAnnotations.java | 64 +++++---- .../MultipleInputStream.java | 60 +++++++++ .../NotOwningCollectionMutationTest.java | 17 +++ .../NotOwningLocal.java | 19 +++ .../OwningCollectionFieldTest.java | 2 +- .../tests/resourceleak-collections/Test.java | 19 ++- 18 files changed, 599 insertions(+), 46 deletions(-) create mode 100644 checker/tests/resourceleak-collections/AnotherBug.java create mode 100644 checker/tests/resourceleak-collections/Bug.java create mode 100644 checker/tests/resourceleak-collections/Buggz.java create mode 100644 checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java create mode 100644 checker/tests/resourceleak-collections/MapGetType.java create mode 100644 checker/tests/resourceleak-collections/MultipleInputStream.java create mode 100644 checker/tests/resourceleak-collections/NotOwningLocal.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index bdca2c90de91..6867a9e83ab8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -29,7 +29,7 @@ public class CollectionOwnershipAnalysis ImmutableSet.of( // Any method call has a CFG edge for Throwable/RuntimeException/Error // to represent run-time misbehavior. Ignore it. - Throwable.class.getCanonicalName(), + // Throwable.class.getCanonicalName(), Error.class.getCanonicalName(), RuntimeException.class.getCanonicalName(), // Use the Nullness Checker to prove this won't happen. diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index e8b8b1323582..51000018e15b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -87,7 +88,20 @@ public TransferResult visitAssignment( TreeUtils.elementFromTree(node.getExpression().getTree()))) { replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); } else { - replaceInStores(res, rhsJE, atypeFactory.NOTOWNINGCOLLECTION); + CollectionOwnershipType declCoType = null; + if (node.getTree() instanceof VariableTree) { + VariableTree varTree = (VariableTree) node.getTree(); + VariableElement vtElement = TreeUtils.elementFromDeclaration(varTree); + if (vtElement != null) { + List vtType = vtElement.asType().getAnnotationMirrors(); + declCoType = atypeFactory.getCoType(vtType); + } + } + if (declCoType == CollectionOwnershipType.NotOwningCollection) { + replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); + } else { + replaceInStores(res, rhsJE, atypeFactory.NOTOWNINGCOLLECTION); + } } break; default: diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 1bdeee108aa4..c39fd13b20b3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -16,6 +16,7 @@ import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; @@ -172,11 +173,17 @@ private boolean isInsertedThingDefinitelyNonOwning( } // annotation-first: explicit @NotOwning - // 1) On the element/variable symbol (locals/params/fields) + // 1) On the element/variable symbol (params/fields) Element e = TreeUtils.elementFromTree(insertedTree); if (e != null && e.getAnnotation(NotOwning.class) != null) { return true; } + + // If it's param and not annotated then treat as notowning + if (e != null && e.getKind() == ElementKind.PARAMETER) { + return e.getAnnotation(Owning.class) == null; + } + // 2) On the return type of a method invocation if (insertedTree instanceof MethodInvocationTree) { ExecutableElement callee = TreeUtils.elementFromUse((MethodInvocationTree) insertedTree); diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 29505661d818..239f230a4305 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -5,3 +5,4 @@ illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. illegal.collection.mutator.owning.insert.into.notowning=Call to %s not allowed on a @NotOwningCollection receiver when inserted value may be owning: %s illegal.collection.mutator.nonowning.insert.into.owning=Call to %s not allowed on an @OwningCollection receiver when inserted value is definitely @NotOwning: %s +collection.obligation.never.enforced=@MustCall method {0} may have never been invoked on the elements of {1} because control cannot reach any method exit after this point. diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 01d4f59bc527..4a26cf96b491 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -222,6 +222,18 @@ public enum MethodExitKind { ImmutableSet.copyOf(EnumSet.allOf(MethodExitKind.class)); } + /** Lazy per-CFG cache: blocks that can reach some method exit. */ + /** CFG currently being analyzed by analyze(cfg). */ + private @Nullable ControlFlowGraph currentCfg = null; + + /** + * Cached set of blocks that can reach an exit block (normal or exceptional), respecting + * getSuccessorsExceptIgnoredExceptions filtering. Computed lazily per CFG, only if needed. + */ + private @Nullable Set blocksThatCanReachExit = null; + + private final Set reportedNeverEnforcedSites = new HashSet<>(); + /** * An Obligation is a dataflow fact: a set of resource aliases and when those resources need to be * cleaned up. Abstractly, each Obligation represents a resource for which the analyzed program @@ -727,6 +739,88 @@ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAn this.countMustCall = checker.hasOption(ResourceLeakChecker.COUNT_MUST_CALL); } + /** + * Ensures blocksThatCanReachExit is computed for currentCfg. Safe to call multiple times; the + * computation runs at most once per CFG. + */ + private void ensureBlocksThatCanReachExitComputed() { + if (blocksThatCanReachExit != null) { + return; + } + if (currentCfg == null) { + throw new BugInCF("ensureBlocksThatCanReachExitComputed called with no currentCfg"); + } + blocksThatCanReachExit = computeBlocksThatCanReachExit(currentCfg); + } + + /** + * Returns true iff {@code b} can reach some method exit (normal or exceptional), along edges that + * are not filtered out by getSuccessorsExceptIgnoredExceptions. + */ + private boolean canReachExit(Block b) { + ensureBlocksThatCanReachExitComputed(); + return blocksThatCanReachExit != null && blocksThatCanReachExit.contains(b); + } + + /** + * Computes the set of blocks that can reach an exit. + * + *

Implementation: 1) enumerate reachable blocks (from entry) to avoid weird unreachable junk + * 2) seed with all exit-like SpecialBlocks (excluding entry) 3) reverse BFS using predecessors, + * but only if the pred->succ edge is one of getSuccessorsExceptIgnoredExceptions(pred) + */ + private Set computeBlocksThatCanReachExit(ControlFlowGraph cfg) { + Block entry = cfg.getEntryBlock(); + Set reachable = RLCCalledMethodsAnnotatedTypeFactory.reachableFrom(entry, 10_000); + + // Identify exit blocks. In CF, normal/exceptional exits are SpecialBlocks. + // Entry is also special, so exclude it explicitly. + // Set exitBlocks = new HashSet<>(); + // for (Block b : reachable) { + // if (b.getType() == BlockType.SPECIAL_BLOCK && b != entry) { + // exitBlocks.add(b); + // } + // } + Block normalExit = cfg.getRegularExitBlock(); + Set exitBlocks = new HashSet<>(); + if (reachable.contains(normalExit)) { + exitBlocks.add(normalExit); + } + + // Reverse reachability from exits. + Set canReachExit = new HashSet<>(exitBlocks); + Deque worklist = new ArrayDeque<>(exitBlocks); + + while (!worklist.isEmpty()) { + Block cur = worklist.removeFirst(); + for (Block pred : cur.getPredecessors()) { + if (!reachable.contains(pred)) { + continue; + } + if (!isAllowedSuccessor(pred, cur)) { + continue; + } + if (canReachExit.add(pred)) { + worklist.addLast(pred); + } + } + } + return canReachExit; + } + + /** + * Returns true iff {@code succ} appears among getSuccessorsExceptIgnoredExceptions(pred). This is + * the key that keeps backward reachability consistent with your forward traversal. + */ + private boolean isAllowedSuccessor(Block pred, Block succ) { + for (IPair p : getSuccessorsExceptIgnoredExceptions(pred)) { + if (p.first == succ) { + return true; + } + } + return false; + } + /** * The main function of the consistency dataflow analysis. The analysis tracks dataflow facts * ("Obligations") of type {@link Obligation}, each representing a set of owning resource aliases @@ -746,6 +840,10 @@ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAn // TODO: This analysis is currently implemented directly using a worklist; in the future, it // should be rewritten to use the dataflow framework of the Checker Framework. public void analyze(ControlFlowGraph cfg) { + // + this.currentCfg = cfg; + this.blocksThatCanReachExit = null; + // The `visited` set contains everything that has been added to the worklist, even if it has // not yet been removed and analyzed. Set visited = new HashSet<>(); @@ -836,6 +934,20 @@ private void addObligationsForCreatesCollectionObligationAnno( obligations.add( CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); } + if (!mustCallValues.isEmpty()) { + // If this call is in a region with no path to method exit, the obligation will never + // be enforced. + if (!canReachExit(node.getBlock())) { + // Deduplication check per call-site tree + if (reportedNeverEnforcedSites.add(node.getTree())) { + checker.reportError( + node.getTree(), + "collection.obligation.never.enforced", + mustCallValues.get(0), + receiverNode.getTree().toString()); + } + } + } } if (receiverIsOwningField) { TreePath currentPath = cmAtf.getPath(node.getTree()); @@ -1940,14 +2052,22 @@ private void checkReassignmentToOwningCollectionField( if (TreeUtils.isConstructor(enclosingMethodTree)) { // If its in the constructor, it may be the first assignment to the field. // TODO: after PR #7050 is merged, use that logic here to determine if first assignment. + // Treat as first assignment into this.field: ownership transfers into the object. + // So the constructor should not be forced to discharge the parameter’s obligation. + if (isOwningCollectionField) { + Set obs = getObligationsForVar(obligations, rhs.getTree()); + for (Obligation o : obs) { + obligations.remove(o); + } + } return; } // TODO: instead of throwing an exception here, we should probably treat it as Owning // collection and issue an error below to be sound. throw new BugInCF( "Expression " + lhs + " cannot be found in CollectionOwnership store " + coStore); - } else if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom - || rhsCoType == CollectionOwnershipType.OwningCollectionBottom) { + } else if (lhsCoType == CollectionOwnershipType.OwningCollectionBottom) { + // Bottom represents null; it’s allowed on RHS of assignment. throw new BugInCF( "Expression " + node diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index cf11cd97edb4..82d922c69605 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -902,7 +902,7 @@ private static final class Edge { } } - private Set reachableFrom(Block entry, int budget) { + public static Set reachableFrom(Block entry, int budget) { Set seen = new HashSet<>(); ArrayDeque q = new ArrayDeque<>(); q.add(entry); diff --git a/checker/tests/resourceleak-collections/AnotherBug.java b/checker/tests/resourceleak-collections/AnotherBug.java new file mode 100644 index 000000000000..468954595b65 --- /dev/null +++ b/checker/tests/resourceleak-collections/AnotherBug.java @@ -0,0 +1,21 @@ +import java.util.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("close") +class OCInCons { + List list; + + public OCInCons(@OwningCollection List list) { + this.list = list; + } + + @CollectionFieldDestructor("this.list") + public void close() { + for (Resource r : list) { + r.close(); + r.flush(); + } + } +} diff --git a/checker/tests/resourceleak-collections/Bug.java b/checker/tests/resourceleak-collections/Bug.java new file mode 100644 index 000000000000..0c263c8fb98a --- /dev/null +++ b/checker/tests/resourceleak-collections/Bug.java @@ -0,0 +1,71 @@ +package com.linkedin.tony.security; + +import java.io.*; +import java.util.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class GetUriReturnType {} + +class ErrorReturnType {} + +class Log { + + public ErrorReturnType error(java.lang.String parameter0, java.lang.Exception parameter1) { + throw new java.lang.Error(); + } +} + +@InheritableMustCall("close") +class FileSystem { + + public GetUriReturnType getUri() { + throw new java.lang.Error(); + } + + public void close() throws java.io.IOException { + throw new java.lang.Error(); + } +} + +class Path { + + public FileSystem getFileSystem() { + throw new java.lang.Error(); + } +} + +class TokenCache { + + private static final Log LOG = null; + + public static void obtainTokensForNamenodes(Path[] paths, String renewer) throws IOException { + @OwningCollection List fsSet = new ArrayList<>(); + try { + for (Path path : paths) { + fsSet.add(path.getFileSystem()); + } + for (FileSystem fs : fsSet) { + try { + obtainTokensForNamenodesInternal(fs, renewer); + } catch (Exception e) { + LOG.error("Errors on getting delegation token for " + fs.getUri(), e); + } + } + } finally { + for (FileSystem fs : fsSet) { + try { + fs.close(); + } catch (Exception e) { + LOG.error("Errors on closing FileSystem " + fs.getUri(), e); + } + } + } + } + + private static void obtainTokensForNamenodesInternal(FileSystem fs, String renewer) + throws IOException { + throw new java.lang.Error(); + } +} diff --git a/checker/tests/resourceleak-collections/Buggz.java b/checker/tests/resourceleak-collections/Buggz.java new file mode 100644 index 000000000000..8463b181dbea --- /dev/null +++ b/checker/tests/resourceleak-collections/Buggz.java @@ -0,0 +1,74 @@ +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class SocketUtil { + + public static Integer[] findUnusedLocalPorts(final int ports) { + Throwable firstFoundExc = null; + final @OwningCollection List socket = new ArrayList(); + final List portsFound = new ArrayList(); + try { + try { + for (int i = 0; i < ports; i++) { + ServerSocket s = new ServerSocket(0); + socket.add(s); + int localPort = s.getLocalPort(); + checkValidPort(localPort); + portsFound.add(localPort); + } + } catch (Throwable e) { + firstFoundExc = e; + final Set searched = new HashSet(); + try { + for (int i = 0; i < ports && portsFound.size() < ports; i++) { + int localPort = findUnusedLocalPort(20000, 65535, searched); + checkValidPort(localPort); + portsFound.add(localPort); + } + } catch (Exception e1) { + Log.log(e1); + } + } finally { + for (ServerSocket s : socket) { + if (s != null) { + try { + s.close(); + } catch (Exception e) { + } + } + } + } + if (portsFound.size() != ports) { + throw firstFoundExc; + } + } catch (Throwable e) { + String message = "Unable to find an unused local port (is there an enabled firewall?)"; + throw new RuntimeException(message, e); + } + return portsFound.toArray(new Integer[portsFound.size()]); + } + + public static void checkValidPort(int port) throws IOException { + throw new java.lang.Error(); + } + + private static int findUnusedLocalPort(int searchFrom, int searchTo, Set searched) { + throw new java.lang.Error(); + } + + public class Log { + + public static CoreException log(Throwable e) { + throw new java.lang.Error(); + } + } + + public class CoreException extends java.lang.RuntimeException {} +} diff --git a/checker/tests/resourceleak-collections/GraphFoundationCrash.java b/checker/tests/resourceleak-collections/GraphFoundationCrash.java index 43b17bddabad..85256ebb18e3 100644 --- a/checker/tests/resourceleak-collections/GraphFoundationCrash.java +++ b/checker/tests/resourceleak-collections/GraphFoundationCrash.java @@ -1,13 +1,36 @@ import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; +@InheritableMustCall("close") class CrashRepro { - // :: error: unfulfilled.field.obligations - private ArrayList pool; + // TODO: Bug ArrayList l = new ArrayList<>() has JDK issues. + private List pool; CrashRepro(int maxSize) { this.pool = new ArrayList<>(maxSize); // should trigger the crash path } + + @CollectionFieldDestructor("this.pool") + void close() { + for (Resource r : pool) { + r.flush(); + r.close(); + } + pool.clear(); + pool = null; + } + + public synchronized void prune() { + // if (pool == null) { + // return; + // } + Iterator itr = pool.iterator(); + while (itr.hasNext()) { + Resource reader = itr.next(); + } + } } diff --git a/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java b/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java new file mode 100644 index 000000000000..a86784f642cd --- /dev/null +++ b/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java @@ -0,0 +1,86 @@ +// import java.io.IOException; +// import java.nio.channels.SocketChannel; +// import java.util.ArrayList; +// import java.util.List; +// +// import org.checkerframework.checker.calledmethods.qual.*; +// import org.checkerframework.checker.collectionownership.qual.*; +// import org.checkerframework.checker.mustcall.qual.*; +// +// class InfiniteLoopGrowingCollection { +// +// void serverLoop() { +// @OwningCollection List socketChannelList = new ArrayList<>(); +// +// while (true) { +// SocketChannel sc = null; +// try { +// // Something that creates an owning resource repeatedly +// sc = SocketChannel.open(); +// sc.configureBlocking(false); +// } catch (IOException e) { +// // swallow: no checked exceptional exit from the method +// } +// +// if (sc != null) { +// // :: error: collection.obligation.never.enforced +// socketChannelList.add(sc); +// } +// } +// } +// } + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class InfiniteLoopGrowingCollection { + + public static void main(String[] args) throws IOException { + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.bind(new InetSocketAddress(8080)); + @OwningCollection List socketChannelList = new ArrayList<>(); + byte[] bytes = new byte[1024]; + ByteBuffer byteBuffer = ByteBuffer.allocate(1024); + serverSocketChannel.configureBlocking(false); + while (true) { + SocketChannel socketChannel = serverSocketChannel.accept(); + if (socketChannel == null) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("无人连接"); + for (SocketChannel item : socketChannelList) { + int len = item.read(byteBuffer); + if (len > 0) { + byteBuffer.flip(); + System.out.println("读取到的数据" + new String(byteBuffer.array(), 0, len)); + } + byteBuffer.clear(); + } + } else { + socketChannel.configureBlocking(false); + // :: error: collection.obligation.never.enforced + socketChannelList.add(socketChannel); + for (SocketChannel item : socketChannelList) { + int len = item.read(byteBuffer); + if (len > 0) { + byteBuffer.flip(); + System.out.println("读取到的数据" + new String(byteBuffer.array(), 0, len)); + } + byteBuffer.clear(); + } + } + } + } +} diff --git a/checker/tests/resourceleak-collections/MapGetType.java b/checker/tests/resourceleak-collections/MapGetType.java new file mode 100644 index 000000000000..4c8d2474ea9a --- /dev/null +++ b/checker/tests/resourceleak-collections/MapGetType.java @@ -0,0 +1,31 @@ +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class ChannelManager { + + private Map name2channel; + + @EnsuresCalledMethods(value = "#1", methods = "close") + public void release(@Owning FileChannel chan) { + try { + chan.close(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public void releaseAll(String prefix) { + @OwningCollection List x = new ArrayList<>(); + for (String fn : name2channel.keySet()) { + if (prefix == null || fn.startsWith(prefix)) { + x.add(name2channel.get(fn)); + } + } + for (FileChannel chan : x) release(chan); + } +} diff --git a/checker/tests/resourceleak-collections/MissingAnnotations.java b/checker/tests/resourceleak-collections/MissingAnnotations.java index 6a5601d8a256..a02b944974f3 100644 --- a/checker/tests/resourceleak-collections/MissingAnnotations.java +++ b/checker/tests/resourceleak-collections/MissingAnnotations.java @@ -1,7 +1,6 @@ import java.io.*; import java.nio.channels.*; import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; import org.checkerframework.checker.calledmethods.qual.*; import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; @@ -16,38 +15,37 @@ private InputStream[] responseSequenceBinary(List fileNames) throws IOEx // :: error: unfulfilled.collection.obligations response.add(new FileInputStream(fileName)); } - // :: error: method.invocation return response.toArray(new InputStream[0]); } } - -@InheritableMustCall("close") -class DestructorWithThis { - private final Queue files; - - DestructorWithThis(int size) { - this.files = new ConcurrentLinkedQueue(); - } - - @CreatesMustCallFor("this") - public void release(RandomAccessFile file) { - this.files.add(file); - } - - @CollectionFieldDestructor("this.files") - public void close() throws IOException { - try { - while (!this.files.isEmpty()) { - RandomAccessFile pooledFile = this.files.poll(); - if (pooledFile != null) { - try { - pooledFile.close(); - } catch (IOException e) { - } - } - } - } finally { - this.files.clear(); - } - } -} +// +// @InheritableMustCall("close") +// class DestructorWithThis { +// private final Queue files; +// +// DestructorWithThis(int size) { +// this.files = new ConcurrentLinkedQueue(); +// } +// +// @CreatesMustCallFor("this") +// public void release(@Owning RandomAccessFile file) { +// this.files.add(file); +// } +// +// @CollectionFieldDestructor("this.files") +// public void close() throws IOException { +// try { +// while (!this.files.isEmpty()) { +// RandomAccessFile pooledFile = this.files.poll(); +// if (pooledFile != null) { +// try { +// pooledFile.close(); +// } catch (IOException e) { +// } +// } +// } +// } finally { +// this.files.clear(); +// } +// } +// } diff --git a/checker/tests/resourceleak-collections/MultipleInputStream.java b/checker/tests/resourceleak-collections/MultipleInputStream.java new file mode 100644 index 000000000000..b885448f7828 --- /dev/null +++ b/checker/tests/resourceleak-collections/MultipleInputStream.java @@ -0,0 +1,60 @@ +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +public class MultipleInputStream extends InputStream { + + private final List streams = new LinkedList<>(); + + private InputStream currentInputStream; + + private int currentIndex; + + public MultipleInputStream(@OwningCollection Collection streams) { + if (streams.size() == 0) { + throw new IllegalArgumentException("At least one stream is required"); + } + this.streams.addAll(streams); + incrementCurrent(); + } + + public MultipleInputStream(InputStream... streams) { + if (streams.length == 0) { + throw new IllegalArgumentException("At least one stream is required"); + } + this.streams.addAll(Arrays.asList(streams)); + incrementCurrent(); + } + + public int read() throws IOException { + throw new java.lang.Error(); + } + + private boolean incrementCurrent() { + if (++currentIndex >= streams.size()) { + return false; + } + currentInputStream = streams.get(currentIndex); + return true; + } + + @NotOwning + Resource nonOwn() { + return null; + } + + Resource[] test() { + + @NotOwningCollection List col = new ArrayList<>(); + col.add(nonOwn()); + Resource[] streamsWithField = new Resource[col.size()]; + streamsWithField = col.toArray(streamsWithField); + return streamsWithField; + } +} diff --git a/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java index 81dc1a077c7c..1135f27e7670 100644 --- a/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java +++ b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java @@ -102,6 +102,10 @@ void ok_noc_field_add_notowning(@NotOwning R r) { cache.add(r); // ok } + void ok_noc_field_add_notowning_2(R r) { + cache.add(r); // also ok + } + void err_noc_field_add_owning_then_close() { R r = new R(); // :: error: illegal.collection.mutator.owning.insert.into.notowning @@ -125,6 +129,19 @@ void err_oc_add_owning_local_no_dispose() { l.add(r); } + void err_oc_add_owning_call_no_dispose() { + List l = new ArrayList<>(); + // :: error: unfulfilled.collection.obligations + l.add(owning()); + } + + void err_oc_add_notowning_call_no_dispose() { + @OwningCollection List l = new ArrayList<>(); + // :: error: illegal.collection.mutator.nonowning.insert.into.owning + // :: error: unfulfilled.collection.obligations + l.add(notowning()); + } + void err_oc_add_owning_alias_no_dispose() { @OwningCollection List l = new ArrayList<>(); R r = new R(); diff --git a/checker/tests/resourceleak-collections/NotOwningLocal.java b/checker/tests/resourceleak-collections/NotOwningLocal.java new file mode 100644 index 000000000000..84d35ada1669 --- /dev/null +++ b/checker/tests/resourceleak-collections/NotOwningLocal.java @@ -0,0 +1,19 @@ +import java.io.InputStream; +import java.util.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class NotOwningLocal { + @NotOwning + InputStream get() { + throw new Error(); + } + + @NotOwningCollection + Iterator test() { + @NotOwningCollection List list = new ArrayList<>(); + list.add(get()); + return list.iterator(); + } +} diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java index a18fb2084b85..90fd1ce3f438 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -42,7 +42,7 @@ public void close() { } // :: error: missing.creates.mustcall.for - void addIllegal(Resource r) { + void addIllegal(@Owning Resource r) { resList.add(r); } diff --git a/checker/tests/resourceleak-collections/Test.java b/checker/tests/resourceleak-collections/Test.java index daf840f8bf2f..8c7e62c5d1be 100644 --- a/checker/tests/resourceleak-collections/Test.java +++ b/checker/tests/resourceleak-collections/Test.java @@ -24,12 +24,23 @@ void add(@Owning Resource2 resource) { @CollectionFieldDestructor("this.tcpConnections") // ::error: contracts.postcondition void shutdownConnections() throws IOException { - Iterator it = tcpConnections.iterator(); - while (it.hasNext()) { - Resource2 r = it.next(); - r.close(); + fieldCloser(this.tcpConnections); + } + + void fieldCloser(@OwningCollection List tcpConnections) { + for (Resource2 r : tcpConnections) { + try { + r.close(); + } catch (IOException e) { + } } } + + void test() { + List ll = new ArrayList<>(); + ll.add(new Resource2()); + ll = this.tcpConnections; + } } @InheritableMustCall("shutdownConnections") From 983ecdb61600e9507fa868deb04e71bc6bc06961 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Tue, 31 Mar 2026 02:32:00 -0700 Subject: [PATCH 347/374] Tests are passing now. more restructuring + documentation remains. --- .../CollectionOwnershipAnalysis.java | 46 +- ...llectionOwnershipAnnotatedTypeFactory.java | 167 +++- .../CollectionOwnershipTransfer.java | 29 +- .../CollectionOwnershipVisitor.java | 104 +- .../collectionownership/messages.properties | 3 +- .../MustCallAnnotatedTypeFactory.java | 119 +++ .../checker/mustcall/MustCallVisitor.java | 212 +++- .../MustCallConsistencyAnalyzer.java | 320 ++---- .../RLCCalledMethodsAnnotatedTypeFactory.java | 940 +++++++++++++----- .../RLCCalledMethodsTransfer.java | 24 +- .../resourceleak-collections/AnotherBug.java | 21 - .../CollectionClearInFinally.java | 38 + .../CollectionFieldDestructorArgPassing.java | 113 +++ .../resourceleak-collections/FinallyBug.java | 28 - .../InfiniteLoopGrowingCollection.java | 100 +- .../LoopBodyAnalysisTest.java | 2 +- .../MissingAnnotations.java | 51 - ...MissingCollectionOwnershipAnnotations.java | 27 + .../MultipleInputStream.java | 27 +- ...cal.java => NotOwningCollectionLocal.java} | 12 +- .../NotOwningCollectionMutationTest.java | 232 +++-- ...undationCrash.java => PoolPruneCrash.java} | 15 +- ....java => SocketUtilThrowableFallback.java} | 6 +- .../tests/resourceleak-collections/Test.java | 94 -- ....java => TokenCacheFileSystemCleanup.java} | 6 +- ...or.java => WhileLoopBodyAnalysisTest.java} | 60 +- checker/tests/resourceleak/IndexMode.java | 8 +- checker/tests/resourceleak/Issue4815.java | 1 - .../tests/resourceleak/SocketIntoList.java | 3 +- 29 files changed, 1672 insertions(+), 1136 deletions(-) delete mode 100644 checker/tests/resourceleak-collections/AnotherBug.java create mode 100644 checker/tests/resourceleak-collections/CollectionClearInFinally.java create mode 100644 checker/tests/resourceleak-collections/CollectionFieldDestructorArgPassing.java delete mode 100644 checker/tests/resourceleak-collections/FinallyBug.java delete mode 100644 checker/tests/resourceleak-collections/MissingAnnotations.java create mode 100644 checker/tests/resourceleak-collections/MissingCollectionOwnershipAnnotations.java rename checker/tests/resourceleak-collections/{NotOwningLocal.java => NotOwningCollectionLocal.java} (57%) rename checker/tests/resourceleak-collections/{GraphFoundationCrash.java => PoolPruneCrash.java} (74%) rename checker/tests/resourceleak-collections/{Buggz.java => SocketUtilThrowableFallback.java} (91%) delete mode 100644 checker/tests/resourceleak-collections/Test.java rename checker/tests/resourceleak-collections/{Bug.java => TokenCacheFileSystemCleanup.java} (91%) rename checker/tests/resourceleak-collections/{WhileLoopDestructor.java => WhileLoopBodyAnalysisTest.java} (76%) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index 6867a9e83ab8..8cf8220cb464 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -1,9 +1,9 @@ package org.checkerframework.checker.collectionownership; import com.google.common.collect.ImmutableSet; -import java.io.UnsupportedEncodingException; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.resourceleak.SetOfTypes; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; @@ -21,37 +21,11 @@ public class CollectionOwnershipAnalysis extends CFAbstractAnalysis { /** - * The set of exceptions to ignore, cached from {@link - * ResourceLeakChecker#getIgnoredExceptions()}. + * The resource-leak ignored-exception policy, except that RLCC does not ignore {@link Throwable}. + * Broad {@code Throwable}-only exceptional paths affect collection-ownership flow in ways that + * matter for RLCC, so this analysis treats them as real exceptional control flow. */ - private static final SetOfTypes DEFAULT_IGNORED_EXCEPTIONS = - SetOfTypes.anyOfTheseNames( - ImmutableSet.of( - // Any method call has a CFG edge for Throwable/RuntimeException/Error - // to represent run-time misbehavior. Ignore it. - // Throwable.class.getCanonicalName(), - Error.class.getCanonicalName(), - RuntimeException.class.getCanonicalName(), - // Use the Nullness Checker to prove this won't happen. - NullPointerException.class.getCanonicalName(), - // These errors can't be predicted statically, so ignore them and assume - // they won't happen. - ClassCircularityError.class.getCanonicalName(), - ClassFormatError.class.getCanonicalName(), - NoClassDefFoundError.class.getCanonicalName(), - OutOfMemoryError.class.getCanonicalName(), - // It's not our problem if the Java type system is wrong. - ClassCastException.class.getCanonicalName(), - // It's not our problem if the code is going to divide by zero. - ArithmeticException.class.getCanonicalName(), - // Use the Index Checker to prevent these errors. - ArrayIndexOutOfBoundsException.class.getCanonicalName(), - NegativeArraySizeException.class.getCanonicalName(), - // Most of the time, this exception is infeasible, as the charset used - // is guaranteed to be present by the Java spec (e.g., "UTF-8"). - // Eventually, this exclusion could be refined by looking at the charset - // being requested. - UnsupportedEncodingException.class.getCanonicalName())); + private final SetOfTypes ignoredExceptions; /** * Creates a new {@link CollectionOwnershipAnalysis}. @@ -62,6 +36,14 @@ public class CollectionOwnershipAnalysis public CollectionOwnershipAnalysis( BaseTypeChecker checker, CollectionOwnershipAnnotatedTypeFactory factory) { super(checker, factory); + ResourceLeakChecker resourceLeakChecker = ResourceLeakUtils.getResourceLeakChecker(checker); + SetOfTypes baseIgnoredExceptions = resourceLeakChecker.getIgnoredExceptions(); + SetOfTypes exactThrowable = + SetOfTypes.anyOfTheseNames(ImmutableSet.of(Throwable.class.getCanonicalName())); + ignoredExceptions = + (types, exceptionType) -> + !exactThrowable.contains(types, exceptionType) + && baseIgnoredExceptions.contains(types, exceptionType); } @Override @@ -86,6 +68,6 @@ public CFValue createAbstractValue(AnnotationMirrorSet annotations, TypeMirror u @Override public boolean isIgnoredExceptionType(TypeMirror exceptionType) { - return DEFAULT_IGNORED_EXCEPTIONS.contains(getTypes(), exceptionType); + return ignoredExceptions.contains(getTypes(), exceptionType); } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 226945fecc98..739c91d83312 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.collectionownership; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; @@ -20,6 +21,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; @@ -31,16 +33,20 @@ import org.checkerframework.checker.collectionownership.qual.OwningCollectionWithoutObligation; import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.MustCallInference; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.JavaExpression; @@ -60,6 +66,7 @@ import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the Collection Ownership Checker. */ public class CollectionOwnershipAnnotatedTypeFactory @@ -123,8 +130,20 @@ public enum CollectionOwnershipType { OwningCollectionBottom }; + /** Classification of the argument inserted by a {@code @CreatesCollectionObligation} mutator. */ + public enum CollectionMutatorArgumentKind { + /** + * The inserted argument is a resource collection, such as in {@code addAll} or {@code putAll}. + */ + BULK_RESOURCE_COLLECTION, + /** The inserted argument is definitely non-owning at the call site. */ + DEFINITELY_NON_OWNING, + /** The inserted argument may be owning or its ownership could not be proven. */ + MAY_BE_OWNING + } + /** - * The method name used for CollectionObligations that represent an obligation of MustCallUnkown. + * The method name used for CollectionObligations that represent an obligation of MustCallUnknown. * The digit in the first character ensures this cannot coincide with an actual method name. */ public static final String UNKNOWN_METHOD_NAME = "1UNKNOWN"; @@ -133,24 +152,27 @@ public enum CollectionOwnershipType { * Maps the AST-tree corresponding to the loop condition of a collection-obligation-fulfilling * loop to the loop wrapper. */ - private static Map conditionToFulfillingLoopMap = - new HashMap<>(); + private static final Map + conditionToVerifiedFulfillingLoopMap = new HashMap<>(); /** * Maps the cfg-block corresponding to the loop conditional block of a * collection-obligation-fulfilling loop to the loop wrapper. */ - private static Map conditionalBlockToFulfillingLoopMap = - new HashMap<>(); + private static final Map + conditionalBlockToVerifiedFulfillingLoopMap = new HashMap<>(); /** * Marks the specified loop as fulfilling a collection obligation. * - * @param loop the loop wrapper + * @param verifiedFulfillingLoop the verified loop wrapper */ - public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { - conditionToFulfillingLoopMap.put(loop.condition, loop); - conditionalBlockToFulfillingLoopMap.put(loop.loopConditionalBlock, loop); + public static void markFulfillingLoop( + ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop) { + conditionToVerifiedFulfillingLoopMap.put( + verifiedFulfillingLoop.condition, verifiedFulfillingLoop); + conditionalBlockToVerifiedFulfillingLoopMap.put( + verifiedFulfillingLoop.loopConditionalBlock, verifiedFulfillingLoop); } /** @@ -159,8 +181,9 @@ public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { * @param tree a tree that is potentially the condition for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given tree is the condition */ - public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) { - return conditionToFulfillingLoopMap.get(tree); + public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForCondition( + Tree tree) { + return conditionToVerifiedFulfillingLoopMap.get(tree); } /** @@ -171,8 +194,9 @@ public static PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) * @return the collection-obligation-fulfilling loop for which the given block is the CFG * conditional block */ - public static PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Block block) { - return conditionalBlockToFulfillingLoopMap.get(block); + public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForConditionalBlock( + Block block) { + return conditionalBlockToVerifiedFulfillingLoopMap.get(block); } /** @@ -274,7 +298,7 @@ private void runResourceLeakPostAnalyze(ControlFlowGraph cfg) { */ public boolean isResourceCollection(TypeMirror t) { List mcValues = getMustCallValuesOfResourceCollectionComponent(t); - return mcValues != null && mcValues.size() > 0; + return mcValues != null && !mcValues.isEmpty(); } /** @@ -376,7 +400,109 @@ public boolean isResourceCollection(Tree tree) { treeMcType = null; } List mcValues = getMustCallValuesOfResourceCollectionComponent(treeMcType); - return mcValues != null && mcValues.size() > 0; + return mcValues != null && !mcValues.isEmpty(); + } + + /** + * Returns true if the given method is annotated {@code @CreatesCollectionObligation}. + * + * @param methodElement a method + * @return true if the method is annotated {@code @CreatesCollectionObligation} + */ + public boolean isCreatesCollectionObligationMethod(ExecutableElement methodElement) { + return getDeclAnnotation( + methodElement, + org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation.class) + != null; + } + + /** + * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} + * call, or null if the call has no such argument. + * + *

The current heuristic is that the inserted argument is the last argument at the call site. + * + * @param tree a method invocation tree + * @return the inserted argument tree, or null if there is none + */ + public @Nullable ExpressionTree getInsertedArgumentTree(MethodInvocationTree tree) { + if (tree.getArguments().isEmpty()) { + return null; + } + return tree.getArguments().get(tree.getArguments().size() - 1); + } + + /** + * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} + * call, or null if the call has no such argument. + * + *

The current heuristic is that the inserted argument is the last argument at the call site. + * + * @param node a method invocation node + * @return the inserted argument node, or null if there is none + */ + public @Nullable Node getInsertedArgumentNode(MethodInvocationNode node) { + List args = node.getArguments(); + if (args.isEmpty()) { + return null; + } + return args.get(args.size() - 1); + } + + /** + * Classifies the argument inserted by a {@code @CreatesCollectionObligation} mutator call. + * + *

For resource collections, this returns {@link + * CollectionMutatorArgumentKind#BULK_RESOURCE_COLLECTION}. For non-collection values, this + * returns {@link CollectionMutatorArgumentKind#DEFINITELY_NON_OWNING} only if ownership is + * definitely absent at the call site. + * + * @param insertedArgumentTree the inserted argument + * @return the inserted argument's ownership classification + */ + public CollectionMutatorArgumentKind getCollectionMutatorArgumentKind(Tree insertedArgumentTree) { + if (insertedArgumentTree == null) { + return CollectionMutatorArgumentKind.MAY_BE_OWNING; + } + + if (isResourceCollection(insertedArgumentTree)) { + return CollectionMutatorArgumentKind.BULK_RESOURCE_COLLECTION; + } + + RLCCalledMethodsAnnotatedTypeFactory rlAtf = + ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); + + Element insertedElement = TreeUtils.elementFromTree(insertedArgumentTree); + if (insertedElement != null && insertedElement.getAnnotation(NotOwning.class) != null) { + return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; + } + + if (insertedElement != null && insertedElement.getKind() == ElementKind.PARAMETER) { + return insertedElement.getAnnotation(Owning.class) == null + ? CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING + : CollectionMutatorArgumentKind.MAY_BE_OWNING; + } + + if (insertedArgumentTree instanceof MethodInvocationTree) { + ExecutableElement callee = + TreeUtils.elementFromUse((MethodInvocationTree) insertedArgumentTree); + if (rlAtf.hasNotOwning(callee)) { + return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; + } + } + + AnnotatedTypeMirror mustCallType = mcAtf.getAnnotatedType(insertedArgumentTree); + if (mustCallType != null && mustCallType.hasPrimaryAnnotation(NotOwning.class)) { + return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; + } + + TypeMirror typeMirror = TreeUtils.typeOf(insertedArgumentTree); + TypeElement typeElement = TypesUtils.getTypeElement(typeMirror); + if (typeElement != null && rlAtf.hasEmptyMustCallValue(typeElement)) { + return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; + } + + return CollectionMutatorArgumentKind.MAY_BE_OWNING; } /** @@ -760,6 +886,15 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { } } } + } else if (elt.getKind() == ElementKind.LOCAL_VARIABLE) { + // Non-resource locals such as "Socket sock = null" can be reconstructed + // as @NotOwningCollection at identifier use sites because of the defaulting path. For + // plain non-collection locals, collection-ownership qualifiers are not meaningful, so + // override that fallback here and keep them at bottom. + AnnotationMirror localAnno = type.getEffectiveAnnotationInHierarchy(TOP); + if (localAnno == null || AnnotationUtils.areSameByName(TOP, localAnno)) { + type.replaceAnnotation(BOTTOM); + } } } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 51000018e15b..e3de6cd273e8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -12,7 +12,7 @@ import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; @@ -121,19 +121,22 @@ public TransferResult visitAssignment( * collection-obligation-fulfilling loop * @return the resulting transfer result */ - private TransferResult updateStoreForPotentiallyFulfillingLoop( - TransferResult res, Tree tree) { - PotentiallyFulfillingLoop loop = + private TransferResult + updateStoreForVerifiedFulfillingCollectionLoop( + TransferResult res, Tree tree) { + ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop = CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); - if (loop != null) { + if (verifiedFulfillingLoop != null) { CollectionOwnershipStore elseStore = res.getElseStore(); - JavaExpression collectionJE = JavaExpression.fromTree(loop.collectionTree); + JavaExpression collectionJE = JavaExpression.fromTree(verifiedFulfillingLoop.collectionTree); - CollectionOwnershipType collectionCoType = atypeFactory.getCoType(loop.collectionTree); + CollectionOwnershipType collectionCoType = + atypeFactory.getCoType(verifiedFulfillingLoop.collectionTree); if (collectionCoType == CollectionOwnershipType.OwningCollection) { List mustCallValuesOfElements = - atypeFactory.getMustCallValuesOfResourceCollectionComponent(loop.collectionTree); - if (loop.getCalledMethods().containsAll(mustCallValuesOfElements)) { + atypeFactory.getMustCallValuesOfResourceCollectionComponent( + verifiedFulfillingLoop.collectionTree); + if (verifiedFulfillingLoop.getCalledMethods().containsAll(mustCallValuesOfElements)) { elseStore.clearValue(collectionJE); elseStore.insertValue(collectionJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); return new ConditionalTransferResult<>( @@ -148,7 +151,7 @@ private TransferResult updateStoreForPotentia public TransferResult visitLessThan( LessThanNode node, TransferInput in) { TransferResult res = super.visitLessThan(node, in); - return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); } @Override @@ -161,7 +164,7 @@ public TransferResult visitMethodInvocation( ExecutableElement method = node.getTarget().getMethod(); List args = node.getArguments(); res = transferOwnershipForMethodInvocation(method, node, args, res); - res = updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + res = updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); // Check whether the method is annotated @CreatesCollectionObligation. ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); @@ -195,14 +198,14 @@ public TransferResult visitMethodInvocation( public TransferResult visitConditionalNot( ConditionalNotNode node, TransferInput in) { TransferResult res = super.visitConditionalNot(node, in); - return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); } @Override public TransferResult visitGreaterThan( GreaterThanNode node, TransferInput in) { TransferResult res = super.visitGreaterThan(node, in); - return updateStoreForPotentiallyFulfillingLoop(res, node.getTree()); + return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index c39fd13b20b3..972f6ae8e69b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -10,13 +10,7 @@ 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; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; -import org.checkerframework.checker.mustcall.MustCallChecker; -import org.checkerframework.checker.mustcall.qual.NotOwning; -import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; @@ -30,7 +24,6 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * The visitor for the Collection Ownership Checker. This visitor is similar to BaseTypeVisitor, but @@ -70,10 +63,9 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { * Enforces the "mutations on non-owning collections" policy for methods annotated * {@code @CreatesCollectionObligation}. * - *

Strategy: (A) Owning receiver (OC or OCWO): the call is a transfer point. It should not be - * used to insert a definitely-non-owning element. (B) NotOwning receiver (NOC): allow mutation - * ONLY if the inserted thing is definitely non-owning. This prevents "smuggling" an owning / - * obligation-carrying element into a non-owning collection. + *

Strategy: a {@code @NotOwningCollection} receiver may only accept an inserted argument that + * is definitely non-owning. Owning receivers are allowed; the resource-leak analysis models any + * collection obligation they create. * *

Note: we intentionally do not add an index property to @CreatesCollectionObligation yet. We * use a heuristic: the "inserted thing" is the last argument at the call site. TODO: Maybe later @@ -82,13 +74,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { */ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) { ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - boolean isCreates = - atypeFactory.getDeclAnnotation( - methodElt, - org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation - .class) - != null; - if (!isCreates) { + if (!atypeFactory.isCreatesCollectionObligationMethod(methodElt)) { return; } ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree); @@ -109,15 +95,18 @@ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) if (recvType == null) { return; } - // Heuristic: inserted resource is at the last index of call - ExpressionTree insertedTree = tree.getArguments().get(tree.getArguments().size() - 1); - RLCCalledMethodsAnnotatedTypeFactory rlAtf = - ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(atypeFactory); - boolean insertedDefinitelyNonOwning = isInsertedThingDefinitelyNonOwning(insertedTree, rlAtf); + ExpressionTree insertedTree = atypeFactory.getInsertedArgumentTree(tree); + if (insertedTree == null) { + return; + } + CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind insertedArgumentKind = + atypeFactory.getCollectionMutatorArgumentKind(insertedTree); String methodName = methodElt.getSimpleName().toString(); switch (recvType) { case NotOwningCollection: - if (!insertedDefinitelyNonOwning) { + if (insertedArgumentKind + != CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind + .DEFINITELY_NON_OWNING) { checker.reportError( insertedTree, "illegal.collection.mutator.owning.insert.into.notowning", @@ -125,19 +114,8 @@ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) TreeUtils.toStringTruncated(insertedTree, 60)); } break; - case OwningCollection: - case OwningCollectionWithoutObligation: - // disallow inserting something that is definitely non-owning into an owning collection. - if (insertedDefinitelyNonOwning) { - checker.reportError( - insertedTree, - "illegal.collection.mutator.nonowning.insert.into.owning", - methodName, - TreeUtils.toStringTruncated(insertedTree, 60)); - } - break; default: - // bottom : ignore + // Owning receivers are handled by resource-leak analysis; bottom is ignored. } } @@ -154,60 +132,6 @@ private CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType getCoTyp return null; } - /** - * Returns true iff the "inserted thing" is definitely non-owning at this call site. - * - *

For non-collection values: - annotation-first: explicit @NotOwning on the - * expression/return/element, if present - fallback: empty @MustCall (no obligation exists) - * - *

For resource-collection values (e.g., addAll/putAll where the "inserted thing" is a - * collection): - annotation-first: the argument must be @NotOwningCollection at the call site - - * we intentionally avoid inspecting element obligations here without extra machinery. - */ - private boolean isInsertedThingDefinitelyNonOwning( - ExpressionTree insertedTree, RLCCalledMethodsAnnotatedTypeFactory rlAtf) { - - // If the inserted thing is itself a resource collection (bulk ops e.g. addAll) - if (atypeFactory.isResourceCollection(insertedTree)) { - return false; - } - - // annotation-first: explicit @NotOwning - // 1) On the element/variable symbol (params/fields) - Element e = TreeUtils.elementFromTree(insertedTree); - if (e != null && e.getAnnotation(NotOwning.class) != null) { - return true; - } - - // If it's param and not annotated then treat as notowning - if (e != null && e.getKind() == ElementKind.PARAMETER) { - return e.getAnnotation(Owning.class) == null; - } - - // 2) On the return type of a method invocation - if (insertedTree instanceof MethodInvocationTree) { - ExecutableElement callee = TreeUtils.elementFromUse((MethodInvocationTree) insertedTree); - if (rlAtf.hasNotOwning(callee)) { - return true; - } - } - // 3) On the type at the tree (covers return-type annotations in many cases) - MustCallAnnotatedTypeFactory mcAtf = rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); - AnnotatedTypeMirror mcType = mcAtf.getAnnotatedType(insertedTree); - if (mcType != null && mcType.hasPrimaryAnnotation(NotOwning.class)) { - return true; - } - - // fallback: empty must-call => no obligation to "smuggle" --- - TypeMirror tm = TreeUtils.typeOf(insertedTree); - TypeElement typeElt = TypesUtils.getTypeElement(tm); - if (typeElt == null) { - return false; - } - // If the type can never have a must-call obligation, treat as definitely safe. - return rlAtf.hasEmptyMustCallValue(typeElt); - } - /** * This method checks that the result type of a constructor is a supertype of the declared type on * the class, if one exists. diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 239f230a4305..74de68f99cbd 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -4,5 +4,4 @@ transfer.owningcollection.field.ownership=Method invocation unsafely transfers t illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. illegal.collection.mutator.owning.insert.into.notowning=Call to %s not allowed on a @NotOwningCollection receiver when inserted value may be owning: %s -illegal.collection.mutator.nonowning.insert.into.owning=Call to %s not allowed on an @OwningCollection receiver when inserted value is definitely @NotOwning: %s -collection.obligation.never.enforced=@MustCall method {0} may have never been invoked on the elements of {1} because control cannot reach any method exit after this point. +collection.obligation.never.enforced=@MustCall method {0} may have never been invoked on the elements of {1} because control cannot reach a normal method exit after this point. diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index fcf8a502e3a5..2688639619f0 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -1,10 +1,12 @@ package org.checkerframework.checker.mustcall; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; @@ -36,12 +38,15 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -154,6 +159,120 @@ public MustCallAnnotatedTypeFactory(BaseTypeChecker checker) { this.postInit(); } + /** + * Records a potentially fulfilling collection loop for the given enclosing method. + * + *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method + * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method + * loop state used by RLCC. + * + * @param enclosingMethodTree the method containing the loop + * @param collectionTree the collection iterated over by the loop + * @param collectionElementTree the tree for the collection element + * @param conditionTree the loop condition + * @param loopBodyEntryBlock the CFG block for the loop body entry + * @param loopConditionalBlock the CFG conditional block for the loop + * @param collectionElementNode the CFG node for the iterated element + */ + public void recordPotentiallyFulfillingCollectionLoop( + MethodTree enclosingMethodTree, + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree conditionTree, + Block loopBodyEntryBlock, + ConditionalBlock loopConditionalBlock, + Node collectionElementNode) { + RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + if (rlccAtf == null) { + return; + } + rlccAtf.recordPotentiallyFulfillingCollectionLoop( + enclosingMethodTree, + collectionTree, + collectionElementTree, + conditionTree, + loopBodyEntryBlock, + loopConditionalBlock, + collectionElementNode); + } + + /** + * Records a potentially fulfilling enhanced-for-loop for the given enclosing method. + * + *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method + * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method + * loop state used by RLCC. + * + * @param enclosingMethodTree the method containing the loop + * @param enhancedForLoopTree the enhanced-for-loop tree + */ + public void recordPotentiallyFulfillingEnhancedForLoop( + MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { + RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + if (rlccAtf == null) { + return; + } + rlccAtf.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, enhancedForLoopTree); + } + + /** + * Records a CFG-resolved potentially fulfilling collection loop for the given enclosing method. + * + *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method + * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method + * loop state used by RLCC. + * + * @param enclosingMethodTree the method containing the loop + * @param collectionTree the collection iterated over by the loop + * @param collectionElementTree the tree for the collection element + * @param conditionTree the loop condition + * @param loopBodyEntryBlock the CFG block for the loop body entry + * @param loopUpdateBlock the CFG block for the loop update + * @param loopConditionalBlock the CFG conditional block for the loop + * @param collectionElementNode the CFG node for the iterated element + */ + public void recordResolvedPotentiallyFulfillingCollectionLoop( + MethodTree enclosingMethodTree, + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree conditionTree, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, + Node collectionElementNode) { + RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + if (rlccAtf == null) { + return; + } + rlccAtf.recordResolvedPotentiallyFulfillingCollectionLoop( + enclosingMethodTree, + collectionTree, + collectionElementTree, + conditionTree, + loopBodyEntryBlock, + loopUpdateBlock, + loopConditionalBlock, + collectionElementNode); + } + + /** + * Returns the RLCC called-methods type factory if this factory is part of the Resource Leak + * Checker hierarchy, or {@code null} otherwise. + * + * @return the RLCC called-methods type factory, or {@code null} + */ + private @Nullable RLCCalledMethodsAnnotatedTypeFactory getRlccAtfIfPartOfHierarchy() { + SourceChecker currentChecker = checker; + while (currentChecker != null) { + String currentCheckerName = currentChecker.getClass().getCanonicalName(); + if (ResourceLeakUtils.rlcCheckers.contains(currentCheckerName)) { + return ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); + } + currentChecker = currentChecker.getParentChecker(); + } + return null; + } + @Override public void setRoot(@Nullable CompilationUnitTree newRoot) { super.setRoot(newRoot); diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 5f206b37f80a..34c5d3ee51c9 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -7,6 +7,7 @@ import com.sun.source.tree.BreakTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ForLoopTree; @@ -45,7 +46,6 @@ import org.checkerframework.checker.mustcall.qual.PolyMustCall; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.dataflow.cfg.block.Block; @@ -357,16 +357,11 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return null; } - // /////////////////////////////////////////////////////////////////////////// - // Syntactically match for-loops that iterate over all elements of a collection on the AST. - // This happens here in the MustCallVisitor instead of the CollectionOwnershipVisitor, which - // would be the natural place to put this logic, because the matching must be completed - // before the CollectionOwnershipTransfer logic runs, and the CollectionOwnershipVisitor runs - // after the CollectionOwnershipTransfer. - /** - * Records, in the {@code @CollectionOwnershipAnnotatedTypeFactory}, loops that call a method on - * entries of an {@code @OwningCollection}. + * Syntactically matches indexed for-loops that iterate over all elements of a collection. + * + *

This logic lives in the Must Call visitor because matching must complete before collection + * ownership transfer runs. */ @Override public Void visitForLoop(ForLoopTree tree, Void p) { @@ -377,18 +372,51 @@ public Void visitForLoop(ForLoopTree tree, Void p) { return super.visitForLoop(tree, p); } - // /////////////////////////////////////////////////////////////////////////// - // AST-only while-loop matching - // - // We only pattern-match on AST here and record a "pendingCfg" loop. - // Later, when CFG exists, we resolve loopUpdateBlock/bodyEntry/conditional/etc. - // /////////////////////////////////////////////////////////////////////////// + /** + * Performs AST-only matching for while-loops that may fulfill collection obligations. + * + *

RLCC resolves the remaining CFG-local loop facts later, during post-analysis of the + * enclosing method. + */ @Override public Void visitWhileLoop(WhileLoopTree tree, Void p) { detectCollectionObligationFulfillingWhileLoop(tree); return super.visitWhileLoop(tree, p); } + @Override + public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { + detectPotentiallyFulfillingEnhancedForLoop(tree); + return super.visitEnhancedForLoop(tree, p); + } + + /** + * Records an enhanced-for-loop that potentially fulfills collection obligations. + * + *

This method only performs AST matching. RLCC resolves the CFG-specific loop facts later, + * during post-analysis of the enclosing method. + * + * @param tree the enhanced-for-loop to inspect + */ + private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree) { + MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + if (enclosingMethodTree == null) { + return; + } + + ExpressionTree collectionTree = collectionTreeFromExpression(tree.getExpression()); + if (collectionTree == null) { + return; + } + + if (!ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(atypeFactory) + .isResourceCollection(collectionTree)) { + return; + } + + atypeFactory.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, tree); + } + /** Condition-kind -> allowed extraction methods. */ private static final class WhileSpec { final Set extractMethods; @@ -440,6 +468,11 @@ private static final class WhileHeaderMatch { } private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { + MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + if (enclosingMethodTree == null) { + return; + } + // 1) Match header ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); WhileHeaderMatch header = matchWhileHeader(condNoParens); @@ -478,7 +511,7 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { ConditionalBlock cblock = findConditionalSuccessor(condBlock); if (cblock == null) { // condition often lives in ExceptionBlocks; try walking up preds and retry - Block peeled = peelExceptionBlocksToPred(condBlock, 50); + Block peeled = peelExceptionBlocksToPred(condBlock); if (peeled != null) { cblock = findConditionalSuccessor(peeled); condBlock = peeled; @@ -496,59 +529,89 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return; } - // 4) Record a "pendingCfg" potentially fulfilling loop. + // 4) Record a potentially fulfilling collection loop. // // We store: // - collectionTree (resources / q / s) // - collectionElementTree (it.next() / q.poll() / s.pop()) // - condition tree (the while condition) // - // We leave CFG fields null and mark pendingCfg=true. - RLCCalledMethodsAnnotatedTypeFactory.addPotentiallyFulfillingLoop( + atypeFactory.recordPotentiallyFulfillingCollectionLoop( + enclosingMethodTree, header.collectionTree, extraction.extractionCall, // IMPORTANT: element is the extraction call tree condNoParens, - /* loopBodyEntryBlock */ loopBodyEntryBlock, - /* loopUpdateBlock */ null, - /* conditionalBlock */ cblock, - /* collectionElementNode */ elementNode, - /* pendingCfg */ true); + loopBodyEntryBlock, + cblock, + elementNode); + } + + /** + * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a + * lambda expression. + * + *

The per-method loop-state refactor records only loops that are part of the enclosing method + * analysis. Lambda-local loop support can be added separately if needed. + * + * @return the enclosing method for the current loop, or {@code null} if it is inside a lambda + */ + private @Nullable MethodTree getEnclosingMethodForCollectionLoop() { + Tree enclosingMethodOrLambda = TreePathUtil.enclosingMethodOrLambda(getCurrentPath()); + if (enclosingMethodOrLambda instanceof MethodTree) { + return (MethodTree) enclosingMethodOrLambda; + } + return null; } private @Nullable Block firstBlockForTree(Tree t) { Set nodes = atypeFactory.getNodesForTree(t); - if (nodes == null || nodes.isEmpty()) return null; + if (nodes == null || nodes.isEmpty()) { + return null; + } for (Node n : nodes) { - Block b = n.getBlock(); - if (b != null) return b; + Block block = n.getBlock(); + if (block != null) { + return block; + } } return null; } private @Nullable Node anyNodeForTree(Tree t) { Set nodes = atypeFactory.getNodesForTree(t); - if (nodes == null || nodes.isEmpty()) return null; + if (nodes == null || nodes.isEmpty()) { + return null; + } return nodes.iterator().next(); } private @Nullable ConditionalBlock findConditionalSuccessor(Block b) { for (Block succ : b.getSuccessors()) { - if (succ instanceof ConditionalBlock) return (ConditionalBlock) succ; + if (succ instanceof ConditionalBlock) { + return (ConditionalBlock) succ; + } } if (b instanceof SingleSuccessorBlock) { Block succ = ((SingleSuccessorBlock) b).getSuccessor(); - if (succ instanceof ConditionalBlock) return (ConditionalBlock) succ; + if (succ instanceof ConditionalBlock) { + return (ConditionalBlock) succ; + } } return null; } - private @Nullable Block peelExceptionBlocksToPred(Block b, int budget) { + private @Nullable Block peelExceptionBlocksToPred(Block b) { Block cur = b; - while (budget-- > 0 && cur instanceof ExceptionBlock) { + Set visitedBlocks = new HashSet<>(); + while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { Set preds = cur.getPredecessors(); - if (preds.size() != 1) break; + if (preds.size() != 1) { + break; + } Block p = preds.iterator().next(); - if (p == null) break; + if (p == null) { + break; + } cur = p; } return cur; @@ -577,13 +640,17 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); WhileHeaderMatch m = matchNonEmptyFromExpr(inner); - if (m != null) return m; + if (m != null) { + return m; + } } // Case B2: while (c.size() > 0) or while (0 < c.size()) if (cond instanceof BinaryTree) { WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); - if (m != null) return m; + if (m != null) { + return m; + } } return null; @@ -598,10 +665,14 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return null; } ExpressionTree recv = receiverOfInvocation(mit); - if (recv == null) return null; + if (recv == null) { + return null; + } Name varName = getNameFromExpressionTree(recv); - if (varName == null) return null; + if (varName == null) { + return null; + } Element recvElt = TreeUtils.elementFromTree(recv); if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { @@ -609,7 +680,9 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { } ExpressionTree colTree = collectionTreeFromExpression(recv); - if (colTree == null) return null; + if (colTree == null) { + return null; + } return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); } @@ -651,10 +724,14 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { } ExpressionTree recv = receiverOfInvocation(sizeCall); - if (recv == null) return null; + if (recv == null) { + return null; + } Name varName = getNameFromExpressionTree(recv); - if (varName == null) return null; + if (varName == null) { + return null; + } Element recvElt = TreeUtils.elementFromTree(recv); if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { @@ -662,7 +739,9 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { } ExpressionTree colTree = collectionTreeFromExpression(recv); - if (colTree == null) return null; + if (colTree == null) { + return null; + } return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); } @@ -686,10 +765,14 @@ private boolean isIsEmptyCall(MethodInvocationTree mit) { /** Recover "col" from: Iterator it = col.iterator(); while (it.hasNext()) { ... } */ private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver(ExpressionTree itExpr) { - if (itExpr == null) return null; + if (itExpr == null) { + return null; + } Element itElt = TreeUtils.elementFromTree(itExpr); - if (!(itElt instanceof VariableElement)) return null; + if (!(itElt instanceof VariableElement)) { + return null; + } // Only recover from local variable declaration with initializer "col.iterator()" if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { @@ -697,14 +780,20 @@ private boolean isIsEmptyCall(MethodInvocationTree mit) { } Tree decl = atypeFactory.declarationFromElement(itElt); - if (!(decl instanceof VariableTree)) return null; + if (!(decl instanceof VariableTree)) { + return null; + } ExpressionTree init = ((VariableTree) decl).getInitializer(); - if (!(init instanceof MethodInvocationTree)) return null; + if (!(init instanceof MethodInvocationTree)) { + return null; + } MethodInvocationTree initCall = (MethodInvocationTree) init; ExpressionTree sel = initCall.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) return null; + if (!(sel instanceof MemberSelectTree)) { + return null; + } MemberSelectTree ms = (MemberSelectTree) sel; if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { @@ -757,10 +846,14 @@ private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { private void recordExtractionIfAny(ExpressionTree expr) { expr = TreeUtils.withoutParens(expr); - if (!(expr instanceof MethodInvocationTree)) return; + if (!(expr instanceof MethodInvocationTree)) { + return; + } MethodInvocationTree mit = (MethodInvocationTree) expr; - if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) return; + if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { + return; + } extractionCount[0]++; if (extractionCount[0] > 1) { @@ -798,7 +891,9 @@ public Void visitAssignment(AssignmentTree node, Void p) { @Override public Void visitVariable(VariableTree vt, Void p) { ExpressionTree init = vt.getInitializer(); - if (init != null) recordExtractionIfAny(init); // T r = it.next() + if (init != null) { + recordExtractionIfAny(init); // T r = it.next() + } return super.visitVariable(vt, p); } @@ -849,6 +944,11 @@ private boolean isExtractionCallOnHeaderVar( * @param tree a `for` loop with exactly one loop variable */ private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { + MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + if (enclosingMethodTree == null) { + return; + } + List loopBodyStatementList; if (tree.getStatement() instanceof BlockTree) { BlockTree blockT = (BlockTree) tree.getStatement(); @@ -899,12 +999,14 @@ private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { if (loopUpdateBlock == null || loopConditionBlock == null) { return; } - // Add the blocks into a static datastructure in the calledmethodsatf, such that it can - // analyze them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds - // the trees to the static datastructure in McoeAtf). + // Record the loop in the RLCCalledMethods ATF's per-method loop state so that it can + // analyze it later. + // MustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop will then + // add verified fulfilling loops to the collection-ownership ATF. Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); - RLCCalledMethodsAnnotatedTypeFactory.addPotentiallyFulfillingLoop( + atypeFactory.recordResolvedPotentiallyFulfillingCollectionLoop( + enclosingMethodTree, collectionTreeFromExpression(collectionElementTree), collectionElementTree, tree.getCondition(), diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4a26cf96b491..2c4de600af6c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -4,7 +4,6 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -44,7 +43,6 @@ import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.collectionownership.CollectionOwnershipStore; -import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -55,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnalysis; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsVisitor; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; @@ -67,7 +65,6 @@ import org.checkerframework.dataflow.cfg.block.Block.BlockType; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.ClassNameNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; @@ -222,15 +219,14 @@ public enum MethodExitKind { ImmutableSet.copyOf(EnumSet.allOf(MethodExitKind.class)); } - /** Lazy per-CFG cache: blocks that can reach some method exit. */ /** CFG currently being analyzed by analyze(cfg). */ private @Nullable ControlFlowGraph currentCfg = null; /** - * Cached set of blocks that can reach an exit block (normal or exceptional), respecting + * Cached set of blocks that can reach a regular exit block, respecting * getSuccessorsExceptIgnoredExceptions filtering. Computed lazily per CFG, only if needed. */ - private @Nullable Set blocksThatCanReachExit = null; + private @Nullable Set blocksThatCanReachRegularExit = null; private final Set reportedNeverEnforcedSites = new HashSet<>(); @@ -740,55 +736,46 @@ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAn } /** - * Ensures blocksThatCanReachExit is computed for currentCfg. Safe to call multiple times; the - * computation runs at most once per CFG. + * Ensures blocksThatCanReachRegularExit is computed for currentCfg. Safe to call multiple times; + * the computation runs at most once per CFG. */ - private void ensureBlocksThatCanReachExitComputed() { - if (blocksThatCanReachExit != null) { + private void ensureBlocksThatCanReachRegularExitComputed() { + if (blocksThatCanReachRegularExit != null) { return; } if (currentCfg == null) { - throw new BugInCF("ensureBlocksThatCanReachExitComputed called with no currentCfg"); + throw new BugInCF("ensureBlocksThatCanReachRegularExitComputed called with no currentCfg"); } - blocksThatCanReachExit = computeBlocksThatCanReachExit(currentCfg); + blocksThatCanReachRegularExit = computeBlocksThatCanReachRegularExit(currentCfg); } /** - * Returns true iff {@code b} can reach some method exit (normal or exceptional), along edges that - * are not filtered out by getSuccessorsExceptIgnoredExceptions. + * Returns true iff {@code b} can reach the regular method exit along edges that are not filtered + * out by getSuccessorsExceptIgnoredExceptions. */ - private boolean canReachExit(Block b) { - ensureBlocksThatCanReachExitComputed(); - return blocksThatCanReachExit != null && blocksThatCanReachExit.contains(b); + private boolean canReachRegularExit(Block b) { + ensureBlocksThatCanReachRegularExitComputed(); + return blocksThatCanReachRegularExit != null && blocksThatCanReachRegularExit.contains(b); } /** - * Computes the set of blocks that can reach an exit. + * Computes the set of blocks that can reach the regular exit. * *

Implementation: 1) enumerate reachable blocks (from entry) to avoid weird unreachable junk - * 2) seed with all exit-like SpecialBlocks (excluding entry) 3) reverse BFS using predecessors, - * but only if the pred->succ edge is one of getSuccessorsExceptIgnoredExceptions(pred) + * 2) seed with the regular exit block, if reachable 3) reverse BFS using predecessors, but only + * if the pred->succ edge is one of getSuccessorsExceptIgnoredExceptions(pred) */ - private Set computeBlocksThatCanReachExit(ControlFlowGraph cfg) { + private Set computeBlocksThatCanReachRegularExit(ControlFlowGraph cfg) { Block entry = cfg.getEntryBlock(); - Set reachable = RLCCalledMethodsAnnotatedTypeFactory.reachableFrom(entry, 10_000); - - // Identify exit blocks. In CF, normal/exceptional exits are SpecialBlocks. - // Entry is also special, so exclude it explicitly. - // Set exitBlocks = new HashSet<>(); - // for (Block b : reachable) { - // if (b.getType() == BlockType.SPECIAL_BLOCK && b != entry) { - // exitBlocks.add(b); - // } - // } + Set reachable = RLCCalledMethodsAnnotatedTypeFactory.reachableFrom(entry); Block normalExit = cfg.getRegularExitBlock(); Set exitBlocks = new HashSet<>(); if (reachable.contains(normalExit)) { exitBlocks.add(normalExit); } - // Reverse reachability from exits. - Set canReachExit = new HashSet<>(exitBlocks); + // Reverse reachability from the regular exit. + Set canReachRegularExit = new HashSet<>(exitBlocks); Deque worklist = new ArrayDeque<>(exitBlocks); while (!worklist.isEmpty()) { @@ -800,12 +787,12 @@ private Set computeBlocksThatCanReachExit(ControlFlowGraph cfg) { if (!isAllowedSuccessor(pred, cur)) { continue; } - if (canReachExit.add(pred)) { + if (canReachRegularExit.add(pred)) { worklist.addLast(pred); } } } - return canReachExit; + return canReachRegularExit; } /** @@ -840,9 +827,8 @@ private boolean isAllowedSuccessor(Block pred, Block succ) { // TODO: This analysis is currently implemented directly using a worklist; in the future, it // should be rewritten to use the dataflow framework of the Checker Framework. public void analyze(ControlFlowGraph cfg) { - // this.currentCfg = cfg; - this.blocksThatCanReachExit = null; + this.blocksThatCanReachRegularExit = null; // The `visited` set contains everything that has been added to the worklist, even if it has // not yet been removed and analyzed. @@ -897,8 +883,7 @@ private void addObligationsForOwningCollectionReturn(Set obligations /** * Adds {@code CollectionObligation}s if the method is annotated - * {@code @CreatesCollectionObligation} and the receiver is currently - * {@code @OwningCollectionWithoutObligation}. + * {@code @CreatesCollectionObligation} and the receiver is currently some owning collection type. * * @param obligations the set of tracked obligations * @param node the method invocation node @@ -906,9 +891,7 @@ private void addObligationsForOwningCollectionReturn(Set obligations private void addObligationsForCreatesCollectionObligationAnno( Set obligations, MethodInvocationNode node) { ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); - boolean hasCreatesCollectionObligation = - coAtf.getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; - if (hasCreatesCollectionObligation) { + if (coAtf.isCreatesCollectionObligationMethod(methodElement)) { Node receiverNode = node.getTarget().getReceiver(); receiverNode = removeCastsAndGetTmpVarIfPresent(receiverNode); boolean receiverIsResourceCollection = coAtf.isResourceCollection(receiverNode.getTree()); @@ -935,9 +918,9 @@ private void addObligationsForCreatesCollectionObligationAnno( CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); } if (!mustCallValues.isEmpty()) { - // If this call is in a region with no path to method exit, the obligation will never - // be enforced. - if (!canReachExit(node.getBlock())) { + // If this call is in a region with no path to the regular method exit, the + // obligation will never be enforced. + if (!canReachRegularExit(node.getBlock())) { // Deduplication check per call-site tree if (reportedNeverEnforcedSites.add(node.getTree())) { checker.reportError( @@ -968,17 +951,21 @@ private void addObligationsForCreatesCollectionObligationAnno( * Models consumption of the inserted element's obligation by the receiver collection for * {@code @CreatesCollectionObligation} calls on owning receivers. * - *

Heuristic: "inserted thing" is the last argument. We only consume when the inserted thing is - * NOT itself a resource collection (i.e., avoid bulk ops). + *

The inserted argument is identified by {@link + * CollectionOwnershipAnnotatedTypeFactory#getInsertedArgumentNode(MethodInvocationNode)}. We only + * consume obligations for single-element inserts, not bulk operations such as {@code addAll} or + * {@code putAll}. */ private void consumeInsertedArgumentObligationIfSingleElementInsert( Set obligations, MethodInvocationNode node) { - List args = node.getArguments(); - if (args.isEmpty()) { + Node inserted = coAtf.getInsertedArgumentNode(node); + if (inserted == null) { return; } - Node inserted = removeCastsAndGetTmpVarIfPresent(args.get(args.size() - 1)); - if (inserted.getTree() != null && coAtf.isResourceCollection(inserted.getTree())) { + inserted = removeCastsAndGetTmpVarIfPresent(inserted); + if (coAtf.getCollectionMutatorArgumentKind(inserted.getTree()) + == CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind + .BULK_RESOURCE_COLLECTION) { // Bulk op (addAll/putAll etc). return; } @@ -2723,13 +2710,13 @@ private void propagateObligationsToSuccessorBlock( // corresponding to the loop condition of a collection-obligation-fulfilling // loop. If yes, don't propagate the collection obligations that are fulfilled // inside the loop. - boolean isElseEdgeOfFulfillingLoop = false; - PotentiallyFulfillingLoop loop = + boolean isElseEdgeOfVerifiedFulfillingLoop = false; + ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop = CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForConditionalBlock(currentBlock); - if ((currentBlock instanceof ConditionalBlock) && loop != null) { + if ((currentBlock instanceof ConditionalBlock) && verifiedFulfillingLoop != null) { ConditionalBlock conditionalBlock = (ConditionalBlock) currentBlock; if (conditionalBlock.getElseSuccessor().equals(successor)) { - isElseEdgeOfFulfillingLoop = true; + isElseEdgeOfVerifiedFulfillingLoop = true; } } @@ -2738,10 +2725,10 @@ private void propagateObligationsToSuccessorBlock( for (Obligation obligation : obligations) { - if (isElseEdgeOfFulfillingLoop) { + if (isElseEdgeOfVerifiedFulfillingLoop) { if (obligation instanceof CollectionObligation) { String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; - if (loop.getCalledMethods().contains(mustCallMethodOfCo)) { + if (verifiedFulfillingLoop.getCalledMethods().contains(mustCallMethodOfCo)) { // don't propagate this obligation along this edge, as it was fulfilled // in the loop that the currentBlock is the conditional block of continue; @@ -3437,172 +3424,14 @@ public static String collectionToString(Collection bwos) { } } - /* - * SECTION: Loop Body Analysis. This section finds loops and analyzes them to determine whether they - * call methods on each element of a collection, which allows for fulfilling collection obligations. - * It reuses much of the cfg traversal logic of the consistency analysis, but is it's own separate - * thing. - */ - - /** - * Traverses the cfg of a method to find and mark enhanced-for-loops that potentially fulfill - * {@code CollectionObligation}s. - * - * @param cfg the cfg of the method to analyze - */ - public void findFulfillingForEachLoops(ControlFlowGraph cfg) { - // The `visited` set contains everything that has been added to the worklist, even if it has - // not yet been removed and analyzed. - Set visited = new HashSet<>(); - Deque worklist = new ArrayDeque<>(); - - // Add any owning parameters to the initial set of variables to track. - BlockWithObligations entry = - new BlockWithObligations(cfg.getEntryBlock(), new HashSet()); - worklist.add(entry); - visited.add(entry); - - while (!worklist.isEmpty()) { - BlockWithObligations current = worklist.remove(); - Block currentBlock = current.block; - - for (IPair successorAndExceptionType : - getSuccessorsExceptIgnoredExceptions(currentBlock)) { - for (Node node : currentBlock.getNodes()) { - if (node instanceof MethodInvocationNode) { - patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg); - } - } - propagate( - new BlockWithObligations(successorAndExceptionType.first, new HashSet()), - visited, - worklist); - } - } - } - - /** - * Calls a loop-body-analysis on the loop if it is desugared from an enhanced for loop. - * - *

If a {@code MethodInvocationNode} is desugared from an enhanced for loop over a collection - * it corresponds to the node in the synthetic {@code Iterator.next()} method call, which is the - * loop update instruction. The AST node corresponding to the loop itself is in this case - * contained as a field in the {@code MethodInvocationNode}, which is set in the CFG translation - * phase one. - * - *

This method now traverses the CFG upwards to find the loop condition and downwards to find - * the first block of the loop body. With these two blocks, it can then call a loop-body-analysis - * to find the methods the loop calls on the elements of the iterated collection, as part of the - * MustCallOnElements checker. - * - * @param methodInvocationNode the {@code MethodInvocationNode}, for which it is checked, whether - * it is desugared from an enhanced for loop - * @param cfg the enclosing cfg of the {@code MethodInvocationNode} - */ - private void patternMatchEnhancedCollectionForLoop( - MethodInvocationNode methodInvocationNode, ControlFlowGraph cfg) { - boolean nodeIsDesugaredFromEnhancedForLoop = - methodInvocationNode.getIterableExpression() != null; - if (nodeIsDesugaredFromEnhancedForLoop) { - // this is the Iterator.next() call desugared from an enhanced-for-loop - EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); - if (loop == null) { - throw new BugInCF( - "MethodInvocationNode.iterableExpression should be non-null iff" - + " MethodInvocationNode.enhancedForLoop is non-null"); - } - - // Find the first block of the loop body. - // Start from the synthetic (desugared) iterator.next() node and traverse the cfg - // until the assignment of the loop iterator variable is found, which is the last - // desugared instruction. The next block is then the start of the loop body. - VariableTree loopVariable = loop.getVariable(); - SingleSuccessorBlock ssblock = (SingleSuccessorBlock) methodInvocationNode.getBlock(); - Iterator nodeIterator = ssblock.getNodes().iterator(); - Node loopVarNode = null; - Node node; - boolean isAssignmentOfIterVar; - do { - while (!nodeIterator.hasNext()) { - ssblock = (SingleSuccessorBlock) ssblock.getSuccessor(); - nodeIterator = ssblock.getNodes().iterator(); - } - node = nodeIterator.next(); - isAssignmentOfIterVar = false; - if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { - loopVarNode = ((AssignmentNode) node).getTarget(); - VariableTree iterVarDecl = (VariableTree) node.getTree(); - isAssignmentOfIterVar = iterVarDecl.getName() == loopVariable.getName(); - } - } while (!isAssignmentOfIterVar); - Block loopBodyEntryBlock = ssblock.getSuccessor(); - - // Find the loop-body-condition - // Start from the synthetic (desugared) iterator.next() node and traverse the cfg - // backwards until the conditional block is found. The previous block is then the block - // containing the desugared loop condition iterator.hasNext(). - Block block = methodInvocationNode.getBlock(); - nodeIterator = block.getNodes().iterator(); - boolean isLoopCondition; - do { - while (!nodeIterator.hasNext()) { - Set predBlocks = block.getPredecessors(); - if (predBlocks.size() == 1) { - block = predBlocks.iterator().next(); - nodeIterator = block.getNodes().iterator(); - } else { - // there is no trivial resolution here. Best we can do is just skip this loop, - // which is of course sound. - return; - // throw new BugInCF( - // "Encountered more than one CFG Block predecessor trying to find the" - // + " enhanced-for-loop update block. Block: "); - } - } - node = nodeIterator.next(); - isLoopCondition = false; - if (node instanceof MethodInvocationNode) { - MethodInvocationTree mit = ((MethodInvocationNode) node).getTree(); - isLoopCondition = TreeUtils.isHasNextCall(mit); - } - } while (!isLoopCondition); - - Block blockContainingLoopCondition = node.getBlock(); - if (blockContainingLoopCondition.getSuccessors().size() != 1) { - throw new BugInCF( - "loop condition has: " - + blockContainingLoopCondition.getSuccessors().size() - + " successors instead of 1."); - } - Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); - if (!(conditionalBlock instanceof ConditionalBlock)) { - throw new BugInCF( - "loop condition successor is not ConditionalBlock, but: " - + conditionalBlock.getClass()); - } - - // add the blocks into a static datastructure in the calledmethodsatf, such that it can - // analyze - // them (call MustCallConsistencyAnalyzer.analyzeFulfillingLoops, which in turn adds the trees - // to the static datastructure in McoeAtf) - PotentiallyFulfillingLoop pfLoop = - new PotentiallyFulfillingLoop( - loop.getExpression(), - loopVarNode.getTree(), - node.getTree(), - loopBodyEntryBlock, - block, - (ConditionalBlock) conditionalBlock, - loopVarNode); - this.analyzeObligationFulfillingLoop(cfg, pfLoop); - } - } + // Loop body analysis verifies collection loops that may call required methods on each iterated + // element, which can satisfy collection obligations. /** - * Analyze the loop body of a 'potentially-mcoe-obligation-fulfilling-loop', as determined by a - * pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or determined by a - * pre-pattern-match in {@code this.patternMatchEnhancedForLoop(MethodInvocationNode)} (in the - * case of an enhanced-for-loop). + * Analyze the loop body of a CFG-resolved potentially fulfilling collection loop, as determined + * by a pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or by a + * pre-pattern-match in the RLCCalledMethodsAnnotatedTypeFactory (in the case of an + * enhanced-for-loop). * *

The analysis uses the CalledMethods type of the collection element iterated over to * determine the methods the loop calls on the collection elements. @@ -3611,26 +3440,29 @@ private void patternMatchEnhancedCollectionForLoop( * postAnalyze(cfg)} method of the {@code RLCCalledMethodsAnnotatedTypeFactory}). * * @param cfg the cfg of the enclosing method - * @param potentiallyFulfillingLoop the loop to check + * @param resolvedPotentiallyFulfillingLoop the loop to check */ - public void analyzeObligationFulfillingLoop( - ControlFlowGraph cfg, PotentiallyFulfillingLoop potentiallyFulfillingLoop) { + public void analyzeResolvedPotentiallyFulfillingCollectionLoop( + ControlFlowGraph cfg, + ResolvedPotentiallyFulfillingCollectionLoop resolvedPotentiallyFulfillingLoop) { // ensure checked loop is initialized in a valid way Objects.requireNonNull( - potentiallyFulfillingLoop.collectionElementTree, + resolvedPotentiallyFulfillingLoop.collectionElementTree, "CollectionElementAccess tree provided to analyze loop body of an" - + " mcoe-obligation-fulfilling loop is null."); + + " CFG-resolved potentially fulfilling collection loop is null."); Objects.requireNonNull( - potentiallyFulfillingLoop.loopBodyEntryBlock, - "Block provided to analyze loop body of an mcoe-obligation-fulfilling loop is null."); + resolvedPotentiallyFulfillingLoop.loopBodyEntryBlock, + "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + + " loop is null."); Objects.requireNonNull( - potentiallyFulfillingLoop.loopUpdateBlock, - "Block provided to analyze loop body of an mcoe-obligation-fulfilling loop is null."); + resolvedPotentiallyFulfillingLoop.loopUpdateBlock, + "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + + " loop is null."); - Block loopBodyEntryBlock = potentiallyFulfillingLoop.loopBodyEntryBlock; - Block loopUpdateBlock = potentiallyFulfillingLoop.loopUpdateBlock; - Tree collectionElement = potentiallyFulfillingLoop.collectionElementTree; + Block loopBodyEntryBlock = resolvedPotentiallyFulfillingLoop.loopBodyEntryBlock; + Block loopUpdateBlock = resolvedPotentiallyFulfillingLoop.loopUpdateBlock; + Tree collectionElement = resolvedPotentiallyFulfillingLoop.collectionElementTree; boolean emptyLoopBody = loopBodyEntryBlock.equals(loopUpdateBlock); if (emptyLoopBody) { @@ -3689,7 +3521,7 @@ public void analyzeObligationFulfillingLoop( if (isLastBlockOfBody) { Set calledMethodsAfterBlock = analyzeTypeOfCollectionElement( - currentBlock, potentiallyFulfillingLoop, obligations, loopUpdateBlock); + currentBlock, resolvedPotentiallyFulfillingLoop, obligations, loopUpdateBlock); // intersect the called methods after this block with the accumulated ones so far. // This is required because there may be multiple "back edges" of the loop, in which // case we must intersect the called methods between those. @@ -3717,10 +3549,10 @@ public void analyzeObligationFulfillingLoop( } } - // now put the loop into the static datastructure if it calls any methods on the element - if (calledMethodsInLoop != null && calledMethodsInLoop.size() > 0) { - potentiallyFulfillingLoop.addCalledMethods(calledMethodsInLoop); - CollectionOwnershipAnnotatedTypeFactory.markFulfillingLoop(potentiallyFulfillingLoop); + // Record the loop as verified if it calls any methods on the iterated element. + if (calledMethodsInLoop != null && !calledMethodsInLoop.isEmpty()) { + resolvedPotentiallyFulfillingLoop.addCalledMethods(calledMethodsInLoop); + CollectionOwnershipAnnotatedTypeFactory.markFulfillingLoop(resolvedPotentiallyFulfillingLoop); } } @@ -3798,7 +3630,7 @@ private Set computeLoopRegion(Block entry, Block update) { * aliases. * * @param lastLoopBodyBlock last block of loop body - * @param potentiallyFulfillingLoop loop wrapper of the loop to analyze + * @param resolvedPotentiallyFulfillingLoop loop wrapper of the loop to analyze * @param obligations the set of tracked obligations * @param loopUpdateBlock block that updates the loop * @return the union of methods in the CalledMethods type of the collection element and all its @@ -3806,7 +3638,7 @@ private Set computeLoopRegion(Block entry, Block update) { */ private Set analyzeTypeOfCollectionElement( Block lastLoopBodyBlock, - PotentiallyFulfillingLoop potentiallyFulfillingLoop, + ResolvedPotentiallyFulfillingCollectionLoop resolvedPotentiallyFulfillingLoop, Set obligations, Block loopUpdateBlock) { AccumulationStore store = null; @@ -3822,7 +3654,7 @@ private Set analyzeTypeOfCollectionElement( store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); } Obligation collectionElementObligation = - getObligationForVar(obligations, potentiallyFulfillingLoop.collectionElementTree); + getObligationForVar(obligations, resolvedPotentiallyFulfillingLoop.collectionElementTree); if (collectionElementObligation == null) { // the loop did something weird. Might have reassigned the collection element. // The sound thing to do is return an empty list. @@ -3838,8 +3670,8 @@ private Set analyzeTypeOfCollectionElement( // add the called methods of the ICE IteratedCollectionElement ice = store.getIteratedCollectionElement( - potentiallyFulfillingLoop.collectionElementNode, - potentiallyFulfillingLoop.collectionElementTree); + resolvedPotentiallyFulfillingLoop.collectionElementNode, + resolvedPotentiallyFulfillingLoop.collectionElementTree); if (ice != null) { AccumulationValue cmValOfIce = store.getValue(ice); List calledMethods = getCalledMethods(cmValOfIce); diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 82d922c69605..b257d450c7c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; @@ -15,8 +16,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Queue; @@ -53,6 +58,9 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -63,6 +71,7 @@ import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.util.Contract; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; @@ -123,70 +132,331 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated private final BiMap tempVarToTree = HashBiMap.create(); /** - * Set of potentially collection-obligation-fulfilling loops. Found in the MustCallVisitor, which - * checks the header and (parts of the) body. + * Per-method loop state for collection-obligation loops that have been matched syntactically by + * the MustCall visitor. */ - private static final Set potentiallyFulfillingLoops = new HashSet<>(); + private static final class MethodCollectionLoopState { + + /** Enhanced-for-loops that have been matched syntactically but still need CFG resolution. */ + final Set potentiallyFulfillingEnhancedForLoops = new LinkedHashSet<>(); + + /** + * Collection-obligation loops that have been matched syntactically but still need CFG-local + * resolution before the consistency analyzer can verify them. + */ + final Set potentiallyFulfillingCollectionLoops = + new LinkedHashSet<>(); + + /** + * Potentially fulfilling collection loops that have all CFG information required by the + * consistency analyzer. + */ + final Set + resolvedPotentiallyFulfillingCollectionLoops = new LinkedHashSet<>(); + + /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ + private @Nullable WhileLoopResolutionCache whileLoopCache; + + /** + * Returns CFG facts for resolving potentially fulfilling while loops in this method, creating + * them lazily if needed. + * + * @param cfg the enclosing method CFG + * @return the CFG facts for resolving potentially fulfilling while loops in this method + */ + private WhileLoopResolutionCache getOrCreateWhileLoopCache(ControlFlowGraph cfg) { + if (whileLoopCache == null) { + whileLoopCache = new WhileLoopResolutionCache(cfg); + } + return whileLoopCache; + } + } + + /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ + private static final class WhileLoopResolutionCache { + + /** A back edge in the CFG. */ + private static final class BlockEdge { + final Block sourceBlock; + final Block targetBlock; + + BlockEdge(Block sourceBlock, Block targetBlock) { + this.sourceBlock = sourceBlock; + this.targetBlock = targetBlock; + } + } + + /** Reachable CFG blocks in the current method. */ + private final Set reachableBlocks; + + /** Back edges among {@link #reachableBlocks}. */ + private final List backEdges; + + /** Natural loops for back edges, computed lazily. */ + private final IdentityHashMap> naturalLoopsByBackEdge = + new IdentityHashMap<>(); + + /** + * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. + * + * @param cfg the enclosing method CFG + */ + private WhileLoopResolutionCache(ControlFlowGraph cfg) { + Block entryBlock = cfg.getEntryBlock(); + this.reachableBlocks = reachableFrom(entryBlock); + Map> dominators = computeDominators(entryBlock, reachableBlocks); + this.backEdges = findBackEdges(reachableBlocks, dominators); + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. + * + * @return the CFG back edges + */ + private List getBackEdges() { + return backEdges; + } + + /** + * Returns the natural loop induced by the given back edge, computing it lazily if needed. + * + * @param backEdge the back edge + * @return the natural loop induced by the given back edge + */ + private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { + return naturalLoopsByBackEdge.computeIfAbsent( + backEdge, + ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); + } + + /** + * Returns blocks reachable from {@code entryBlock}. + * + * @param entryBlock the CFG entry block + * @return the reachable blocks + */ + private static Set reachableFrom(Block entryBlock) { + Set seen = new HashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(entryBlock); + seen.add(entryBlock); + + while (!queue.isEmpty()) { + Block block = queue.remove(); + for (Block successor : block.getSuccessors()) { + if (successor != null && seen.add(successor)) { + queue.add(successor); + } + } + } + return seen; + } + + /** + * Computes dominators for the reachable blocks in the current CFG. + * + * @param entryBlock the CFG entry block + * @param reachableBlocks reachable blocks in the CFG + * @return dominators for each reachable block + */ + private static Map> computeDominators( + Block entryBlock, Set reachableBlocks) { + Map> dominators = new HashMap<>(); + + for (Block block : reachableBlocks) { + if (block == entryBlock) { + dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); + } else { + dominators.put(block, new HashSet<>(reachableBlocks)); // TOP + } + } + + boolean changed; + do { + changed = false; + for (Block block : reachableBlocks) { + if (block == entryBlock) { + continue; + } + + Set newDominators = null; + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + Set predecessorDominators = dominators.get(predecessor); + if (predecessorDominators == null) { + continue; + } + if (newDominators == null) { + newDominators = new HashSet<>(predecessorDominators); + } else { + newDominators.retainAll(predecessorDominators); + } + } + + if (newDominators == null) { + newDominators = new HashSet<>(); + } + newDominators.add(block); + + if (!newDominators.equals(dominators.get(block))) { + dominators.put(block, newDominators); + changed = true; + } + } + } while (changed); + + return dominators; + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. + * + * @param reachableBlocks reachable blocks in the CFG + * @param dominators dominators for each reachable block + * @return the CFG back edges + */ + private static List findBackEdges( + Set reachableBlocks, Map> dominators) { + List backEdges = new ArrayList<>(); + for (Block sourceBlock : reachableBlocks) { + for (Block targetBlock : sourceBlock.getSuccessors()) { + if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { + continue; + } + Set sourceDominators = dominators.get(sourceBlock); + if (sourceDominators != null && sourceDominators.contains(targetBlock)) { + // targetBlock dominates sourceBlock, so sourceBlock -> targetBlock is a back edge. + backEdges.add(new BlockEdge(sourceBlock, targetBlock)); + } + } + } + return backEdges; + } + + /** + * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. + * + * @param sourceBlock the source of the back edge + * @param targetBlock the target of the back edge + * @param reachableBlocks reachable blocks in the CFG + * @return the natural loop induced by the back edge + */ + private static Set naturalLoop( + Block sourceBlock, Block targetBlock, Set reachableBlocks) { + Set loopBlocks = new HashSet<>(); + ArrayDeque stack = new ArrayDeque<>(); + + loopBlocks.add(targetBlock); + if (loopBlocks.add(sourceBlock)) { + stack.push(sourceBlock); + } + + while (!stack.isEmpty()) { + Block block = stack.pop(); + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + if (loopBlocks.add(predecessor) && predecessor != targetBlock) { + stack.push(predecessor); + } + } + } + return loopBlocks; + } + } + + /** Per-method collection-loop state accumulated during MustCall visitor matching. */ + private final IdentityHashMap + collectionLoopStateByEnclosingMethod = new IdentityHashMap<>(); /** - * Construct a {@code PotentiallyFulfillingLoop} and add it to the static set of such loops to - * have their loop body analyzed for possibly fulfilling collection obligations. + * Returns the loop state for the given method, creating it if needed. * - * @param collectionTree AST {@code Tree} for collection iterated over - * @param collectionElementTree AST {@code Tree} for collection element iterated over - * @param condition AST {@code Tree} for loop condition - * @param loopBodyEntryBlock cfg {@code Block} for loop body entry - * @param loopUpdateBlock {@code Block} containing loop update - * @param loopConditionalBlock {@code Block} containing loop condition - * @param collectionEltNode cfg {@code Node} for collection element iterated over + * @param enclosingMethodTree the enclosing method + * @return the loop state for the given method */ - public static void addPotentiallyFulfillingLoop( - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree condition, - Block loopBodyEntryBlock, - Block loopUpdateBlock, - ConditionalBlock loopConditionalBlock, - Node collectionEltNode) { - potentiallyFulfillingLoops.add( - new PotentiallyFulfillingLoop( - collectionTree, - collectionElementTree, - condition, - loopBodyEntryBlock, - loopUpdateBlock, - loopConditionalBlock, - collectionEltNode)); - } - - public static void addPotentiallyFulfillingLoop( - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree condition, - Block loopBodyEntryBlock, - Block loopUpdateBlock, - ConditionalBlock loopConditionalBlock, - Node collectionEltNode, - boolean pendingCfg) { - potentiallyFulfillingLoops.add( - new PotentiallyFulfillingLoop( - collectionTree, - collectionElementTree, - condition, - loopBodyEntryBlock, - loopUpdateBlock, - loopConditionalBlock, - collectionEltNode, - pendingCfg)); + private MethodCollectionLoopState getOrCreateMethodCollectionLoopState( + MethodTree enclosingMethodTree) { + return collectionLoopStateByEnclosingMethod.computeIfAbsent( + enclosingMethodTree, ignored -> new MethodCollectionLoopState()); } /** - * Returns the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis. + * Returns the loop state for the given underlying AST, or {@code null} if there is none. * - * @return the static set of {@code PotentiallyFulfillingLoop}s scheduled for analysis + * @param underlyingAST the current underlying AST + * @return the loop state for the given underlying AST, or {@code null} */ - public static Set getPotentiallyFulfillingLoops() { - return potentiallyFulfillingLoops; + private @Nullable MethodCollectionLoopState getMethodCollectionLoopState( + UnderlyingAST underlyingAST) { + MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); + if (enclosingMethodTree == null) { + return null; + } + return collectionLoopStateByEnclosingMethod.get(enclosingMethodTree); + } + + /** + * Returns the potentially fulfilling collection loops for the method represented by the given + * underlying AST. + * + * @param underlyingAST the current underlying AST + * @return the potentially fulfilling collection loops for the method represented by the given + * underlying AST + */ + Set getPotentiallyFulfillingCollectionLoops( + UnderlyingAST underlyingAST) { + MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); + if (loopState == null) { + return Collections.emptySet(); + } + return loopState.potentiallyFulfillingCollectionLoops; + } + + /** + * Returns the resolved potentially fulfilling collection loops for the method represented by the + * given underlying AST. + * + * @param underlyingAST the current underlying AST + * @return the resolved potentially fulfilling collection loops for the method represented by the + * given underlying AST + */ + Set getResolvedPotentiallyFulfillingCollectionLoops( + UnderlyingAST underlyingAST) { + MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); + if (loopState == null) { + return Collections.emptySet(); + } + return loopState.resolvedPotentiallyFulfillingCollectionLoops; + } + + /** + * Removes the loop state associated with the given underlying AST. + * + * @param underlyingAST the current underlying AST + */ + private void removeMethodCollectionLoopState(UnderlyingAST underlyingAST) { + MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); + if (enclosingMethodTree != null) { + collectionLoopStateByEnclosingMethod.remove(enclosingMethodTree); + } + } + + /** + * Returns the enclosing method for the given underlying AST, or {@code null} if the underlying + * AST is not a method. + * + * @param underlyingAST the current underlying AST + * @return the enclosing method for the given underlying AST, or {@code null} + */ + private @Nullable MethodTree getEnclosingMethodTree(UnderlyingAST underlyingAST) { + if (underlyingAST.getKind() != UnderlyingAST.Kind.METHOD) { + return null; + } + return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); } /** @@ -255,6 +525,55 @@ protected ControlFlowGraph analyze( capturedStore); } + public void recordPotentiallyFulfillingCollectionLoop( + MethodTree enclosingMethodTree, + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree conditionTree, + Block loopBodyEntryBlock, + ConditionalBlock loopConditionalBlock, + Node collectionElementNode) { + getOrCreateMethodCollectionLoopState(enclosingMethodTree) + .potentiallyFulfillingCollectionLoops + .add( + new PotentiallyFulfillingCollectionLoop( + collectionTree, + collectionElementTree, + conditionTree, + loopBodyEntryBlock, + loopConditionalBlock, + collectionElementNode)); + } + + public void recordPotentiallyFulfillingEnhancedForLoop( + MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { + getOrCreateMethodCollectionLoopState(enclosingMethodTree) + .potentiallyFulfillingEnhancedForLoops + .add(enhancedForLoopTree); + } + + public void recordResolvedPotentiallyFulfillingCollectionLoop( + MethodTree enclosingMethodTree, + ExpressionTree collectionTree, + Tree collectionElementTree, + Tree conditionTree, + Block loopBodyEntryBlock, + Block loopUpdateBlock, + ConditionalBlock loopConditionalBlock, + Node collectionElementNode) { + getOrCreateMethodCollectionLoopState(enclosingMethodTree) + .resolvedPotentiallyFulfillingCollectionLoops + .add( + new ResolvedPotentiallyFulfillingCollectionLoop( + collectionTree, + collectionElementTree, + conditionTree, + loopBodyEntryBlock, + loopUpdateBlock, + loopConditionalBlock, + collectionElementNode)); + } + @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); @@ -693,7 +1012,7 @@ public TransferInput getInput(Block block) } /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ - public static class PotentiallyFulfillingLoop { + public static class PotentiallyFulfillingCollectionLoop { /** AST {@code Tree} for collection iterated over. */ public final ExpressionTree collectionTree; @@ -704,81 +1023,85 @@ public static class PotentiallyFulfillingLoop { /** AST {@code Tree} for loop condition. */ public final Tree condition; - /** - * The methods that the loop definitely calls on all elements of the collection it iterates - * over. - */ - protected final Set calledMethods; - /** cfg {@code Block} containing the loop body entry. */ public final Block loopBodyEntryBlock; - /** cfg {@code Block} containing the loop update. */ - public Block loopUpdateBlock; - /** cfg conditional {@link Block} following loop condition. */ public final ConditionalBlock loopConditionalBlock; /** cfg {@code Node} for the collection element iterated over. */ public final Node collectionElementNode; - public boolean pendingCfg = false; - /** - * Constructs a new {@code PotentiallyFulfillingLoop} + * Constructs a new {@code PotentiallyFulfillingCollectionLoop}. * * @param collectionTree AST {@link Tree} for collection iterated over * @param collectionElementTree AST {@link Tree} for collection element iterated over * @param condition AST {@link Tree} for loop condition * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry - * @param loopUpdateBlock cfg {@link Block} for the loop update * @param loopConditionalBlock cfg conditional {@link Block} following loop condition * @param collectionEltNode cfg {@link Node} for the collection element iterated over */ - public PotentiallyFulfillingLoop( + public PotentiallyFulfillingCollectionLoop( ExpressionTree collectionTree, Tree collectionElementTree, Tree condition, Block loopBodyEntryBlock, - Block loopUpdateBlock, ConditionalBlock loopConditionalBlock, Node collectionEltNode) { this.collectionTree = collectionTree; this.collectionElementTree = collectionElementTree; this.condition = condition; - this.calledMethods = new HashSet<>(); this.loopBodyEntryBlock = loopBodyEntryBlock; - this.loopUpdateBlock = loopUpdateBlock; this.loopConditionalBlock = loopConditionalBlock; this.collectionElementNode = collectionEltNode; } + } + + /** + * A potentially fulfilling collection loop whose CFG-local information is complete enough for + * consistency analysis. + */ + public static class ResolvedPotentiallyFulfillingCollectionLoop + extends PotentiallyFulfillingCollectionLoop { - public PotentiallyFulfillingLoop( + /** + * The methods that the loop definitely calls on all elements of the collection it iterates + * over. + */ + protected final Set calledMethods; + + /** cfg {@code Block} containing the loop update. */ + public final Block loopUpdateBlock; + + /** + * Constructs a new {@code ResolvedPotentiallyFulfillingCollectionLoop}. + * + * @param collectionTree AST {@link Tree} for collection iterated over + * @param collectionElementTree AST {@link Tree} for collection element iterated over + * @param condition AST {@link Tree} for loop condition + * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry + * @param loopUpdateBlock cfg {@link Block} for the loop update + * @param loopConditionalBlock cfg conditional {@link Block} following loop condition + * @param collectionEltNode cfg {@link Node} for the collection element iterated over + */ + public ResolvedPotentiallyFulfillingCollectionLoop( ExpressionTree collectionTree, Tree collectionElementTree, Tree condition, Block loopBodyEntryBlock, Block loopUpdateBlock, ConditionalBlock loopConditionalBlock, - Node collectionEltNode, - boolean pendingCfg) { - this.collectionTree = collectionTree; - this.collectionElementTree = collectionElementTree; - this.condition = condition; + Node collectionEltNode) { + super( + collectionTree, + collectionElementTree, + condition, + loopBodyEntryBlock, + loopConditionalBlock, + collectionEltNode); this.calledMethods = new HashSet<>(); - this.loopBodyEntryBlock = loopBodyEntryBlock; this.loopUpdateBlock = loopUpdateBlock; - this.loopConditionalBlock = loopConditionalBlock; - this.collectionElementNode = collectionEltNode; - this.pendingCfg = pendingCfg; - } - - public boolean isResolved() { - return !pendingCfg - && loopBodyEntryBlock != null - && loopUpdateBlock != null - && loopConditionalBlock != null - && collectionElementNode != null; } /** @@ -803,9 +1126,9 @@ public Set getCalledMethods() { } /** - * After running the called-methods analysis, call the consistency analyzer to analyze the loop - * bodys of 'potentially-collection-obligation-fulfilling-loops', as determined by a - * pre-pattern-match in the MustCallVisitor. + * After running the called-methods analysis, call the consistency analyzer to analyze + * CFG-resolved potentially fulfilling collection loops, as determined by a pre-pattern-match in + * the MustCallVisitor. * *

The analysis uses the CalledMethods type of the collection element iterated over to * determine the methods the loop calls on the collection elements. @@ -816,180 +1139,353 @@ public Set getCalledMethods() { public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); + MethodCollectionLoopState loopState = getMethodCollectionLoopState(cfg.getUnderlyingAST()); - resolvePendingWhileLoopUpdateBlocks(cfg); - - // traverse the cfg to find enhanced-for-loops over collections and perform a - // loop-body-analysis. - // since this runs before the consistency analysis, we unfortunately have to traverse the cfg - // twice. The first time to find these for-each loop, and the second time much later to do the - // final consistency analysis. - mustCallConsistencyAnalyzer.findFulfillingForEachLoops(cfg); - - // perform loop-body-analysis on normal for-loops that were pattern matched on the AST - if (potentiallyFulfillingLoops.size() > 0) { - Set analyzed = new HashSet<>(); - for (PotentiallyFulfillingLoop potentiallyFulfillingLoop : potentiallyFulfillingLoops) { - Tree collectionElementTree = potentiallyFulfillingLoop.collectionElementTree; - boolean loopContainedInThisMethod = - cfg.getNodesCorrespondingToTree(collectionElementTree) != null; - if (loopContainedInThisMethod) { - mustCallConsistencyAnalyzer.analyzeObligationFulfillingLoop( - cfg, potentiallyFulfillingLoop); - analyzed.add(potentiallyFulfillingLoop); - } + if (loopState != null) { + if (!loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) { + new EnhancedForLoopResolver(cfg, loopState).resolveEnhancedForLoops(); } - potentiallyFulfillingLoops.removeAll(analyzed); + new WhileLoopResolver(cfg).resolveWhileLoops(loopState); + analyzeResolvedPotentiallyFulfillingCollectionLoops( + cfg, loopState, mustCallConsistencyAnalyzer); } super.postAnalyze(cfg); + removeMethodCollectionLoopState(cfg.getUnderlyingAST()); } - public void resolvePendingWhileLoopUpdateBlocks(ControlFlowGraph cfg) { - Block entry = cfg.getEntryBlock(); - Set blocks = reachableFrom(entry, 500); - - Map> dom = computeDominators(entry, blocks); - List backEdges = findBackEdges(blocks, dom); + /** + * Returns blocks reachable from {@code entryBlock}. + * + *

This remains a utility on the outer type because the resource leak consistency analyzer also + * uses it. + * + * @param entryBlock the CFG entry block + * @return the reachable blocks + */ + public static Set reachableFrom(Block entryBlock) { + return WhileLoopResolutionCache.reachableFrom(entryBlock); + } - for (PotentiallyFulfillingLoop loop : getPotentiallyFulfillingLoops()) { - if (!loop.pendingCfg) continue; - if (loop.loopConditionalBlock == null || loop.loopBodyEntryBlock == null) continue; + /** + * Analyzes CFG-resolved potentially fulfilling collection loops for the current method and + * removes the ones that were analyzed. + * + * @param cfg the CFG of the current method + * @param loopState per-method collection-loop state + * @param mustCallConsistencyAnalyzer the consistency analyzer + */ + private void analyzeResolvedPotentiallyFulfillingCollectionLoops( + ControlFlowGraph cfg, + MethodCollectionLoopState loopState, + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer) { + if (loopState.resolvedPotentiallyFulfillingCollectionLoops.isEmpty()) { + return; + } - Block header = chooseHeaderForLoop(loop, backEdges, blocks); - if (header != null) { - loop.loopUpdateBlock = header; // back-edge target = loop "header" - loop.pendingCfg = false; + Iterator resolvedLoopIterator = + loopState.resolvedPotentiallyFulfillingCollectionLoops.iterator(); + while (resolvedLoopIterator.hasNext()) { + ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = resolvedLoopIterator.next(); + Tree collectionElementTree = resolvedLoop.collectionElementTree; + boolean loopContainedInThisMethod = + cfg.getNodesCorrespondingToTree(collectionElementTree) != null; + if (loopContainedInThisMethod) { + mustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop( + cfg, resolvedLoop); + resolvedLoopIterator.remove(); } } } - private @Nullable Block chooseHeaderForLoop( - PotentiallyFulfillingLoop loop, List backEdges, Set blocks) { + /** Resolves enhanced-for-loop candidates into CFG-resolved loops for consistency analysis. */ + private final class EnhancedForLoopResolver { + /** The CFG of the current method. */ + private final ControlFlowGraph cfg; - Block bodyEntry = loop.loopBodyEntryBlock; - Block condBlock = loop.loopConditionalBlock; + /** Per-method collection-loop state. */ + private final MethodCollectionLoopState loopState; - Block bestHeader = null; - int bestSize = Integer.MAX_VALUE; + /** Blocks that have already been visited while traversing the CFG. */ + private final Set visitedBlocks = new HashSet<>(); - for (Edge e : backEdges) { - // e.v is header candidate (dominates e.u) - Set natural = naturalLoop(e.u, e.v, blocks, 100000); + /** Worklist for CFG traversal. */ + private final Deque worklist = new ArrayDeque<>(); - // Must contain this while’s body entry and conditional block - if (!natural.contains(bodyEntry)) continue; - if (!natural.contains(condBlock)) continue; - - // Prefer tightest loop (helps nested-loop disambiguation) - if (natural.size() < bestSize) { - bestSize = natural.size(); - bestHeader = e.v; - } + /** + * Creates a resolver for enhanced-for-loops in the given CFG. + * + * @param cfg the CFG of the current method + * @param loopState per-method collection-loop state + */ + private EnhancedForLoopResolver(ControlFlowGraph cfg, MethodCollectionLoopState loopState) { + this.cfg = cfg; + this.loopState = loopState; } - return bestHeader; - } - - // ----- Dominators / natural-loop machinery ----- + /** Traverses the CFG and records resolved enhanced-for-loops for the pending candidates. */ + private void resolveEnhancedForLoops() { + Block entryBlock = cfg.getEntryBlock(); + worklist.add(entryBlock); + visitedBlocks.add(entryBlock); - private static final class Edge { - final Block u, v; // u -> v + while (!worklist.isEmpty() && !loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) { + Block currentBlock = worklist.removeFirst(); - Edge(Block u, Block v) { - this.u = u; - this.v = v; - } - } - - public static Set reachableFrom(Block entry, int budget) { - Set seen = new HashSet<>(); - ArrayDeque q = new ArrayDeque<>(); - q.add(entry); - seen.add(entry); + for (Node node : currentBlock.getNodes()) { + if (node instanceof MethodInvocationNode) { + resolveEnhancedForLoop((MethodInvocationNode) node); + } + } - while (!q.isEmpty() && budget-- > 0) { - Block b = q.remove(); - for (Block s : b.getSuccessors()) { - if (s != null && seen.add(s)) { - q.add(s); + for (IPair successorAndExceptionType : + getSuccessorsExceptIgnoredExceptions(currentBlock)) { + Block successorBlock = successorAndExceptionType.first; + if (successorBlock != null && visitedBlocks.add(successorBlock)) { + worklist.addLast(successorBlock); + } } } } - return seen; - } - private Map> computeDominators(Block entry, Set blocks) { - Map> dom = new HashMap<>(); - - for (Block b : blocks) { - if (b == entry) { - dom.put(b, new HashSet<>(Collections.singleton(entry))); + /** + * Returns all successor blocks for some block, except for those corresponding to ignored + * exception types. See {@link RLCCalledMethodsAnalysis#isIgnoredExceptionType(TypeMirror)}. + * + * @param block input block + * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for + * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor + */ + private Set> getSuccessorsExceptIgnoredExceptions( + Block block) { + if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { + ExceptionBlock exceptionBlock = (ExceptionBlock) block; + Set> result = new LinkedHashSet<>(); + Block regularSuccessor = exceptionBlock.getSuccessor(); + if (regularSuccessor != null) { + result.add(IPair.of(regularSuccessor, null)); + } + Map> exceptionalSuccessors = + exceptionBlock.getExceptionalSuccessors(); + for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { + TypeMirror exceptionType = entry.getKey(); + if (!isIgnoredExceptionType(exceptionType)) { + for (Block exceptionalSuccessor : entry.getValue()) { + result.add(IPair.of(exceptionalSuccessor, exceptionType)); + } + } + } + return result; } else { - dom.put(b, new HashSet<>(blocks)); // TOP + Set> result = new LinkedHashSet<>(); + for (Block successorBlock : block.getSuccessors()) { + result.add(IPair.of(successorBlock, null)); + } + return result; } } - boolean changed; - do { - changed = false; - for (Block b : blocks) { - if (b == entry) continue; - - Set newDom = null; - for (Block p : b.getPredecessors()) { - if (p == null || !blocks.contains(p)) continue; - Set pDom = dom.get(p); - if (pDom == null) continue; - if (newDom == null) newDom = new HashSet<>(pDom); - else newDom.retainAll(pDom); - } + /** + * Records a resolved collection loop if the given node is desugared from an enhanced-for-loop + * over a collection. + * + * @param methodInvocationNode the node to check + */ + private void resolveEnhancedForLoop(MethodInvocationNode methodInvocationNode) { + if (methodInvocationNode.getIterableExpression() == null) { + return; + } - if (newDom == null) newDom = new HashSet<>(); - newDom.add(b); + EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); + if (loop == null) { + throw new BugInCF( + "MethodInvocationNode.iterableExpression should be non-null iff" + + " MethodInvocationNode.enhancedForLoop is non-null"); + } + if (!loopState.potentiallyFulfillingEnhancedForLoops.contains(loop)) { + return; + } - if (!newDom.equals(dom.get(b))) { - dom.put(b, newDom); - changed = true; + VariableTree loopVariable = loop.getVariable(); + + // Find the first block of the loop body by traversing the desugared iterator.next() path + // until the assignment of the loop variable is found. + SingleSuccessorBlock singleSuccessorBlock = + (SingleSuccessorBlock) methodInvocationNode.getBlock(); + Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); + Node loopVariableNode = null; + Node node; + boolean isAssignmentOfLoopVariable; + do { + while (!nodeIterator.hasNext()) { + singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); + nodeIterator = singleSuccessorBlock.getNodes().iterator(); + } + node = nodeIterator.next(); + isAssignmentOfLoopVariable = false; + if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { + loopVariableNode = ((AssignmentNode) node).getTarget(); + VariableTree iteratorVariableDeclaration = (VariableTree) node.getTree(); + isAssignmentOfLoopVariable = + iteratorVariableDeclaration.getName() == loopVariable.getName(); + } + } while (!isAssignmentOfLoopVariable); + Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); + + // Find the desugared loop condition by traversing the CFG backwards until iterator.hasNext() + // is found. + Block loopUpdateBlock = methodInvocationNode.getBlock(); + nodeIterator = loopUpdateBlock.getNodes().iterator(); + boolean isLoopCondition; + do { + while (!nodeIterator.hasNext()) { + Set predecessorBlocks = loopUpdateBlock.getPredecessors(); + if (predecessorBlocks.size() == 1) { + loopUpdateBlock = predecessorBlocks.iterator().next(); + nodeIterator = loopUpdateBlock.getNodes().iterator(); + } else { + // There is no trivial resolution here. Best we can do is skip this loop. + return; + } + } + node = nodeIterator.next(); + isLoopCondition = false; + if (node instanceof MethodInvocationNode) { + MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); + isLoopCondition = TreeUtils.isHasNextCall(methodInvocationTree); } + } while (!isLoopCondition); + + Block blockContainingLoopCondition = node.getBlock(); + if (blockContainingLoopCondition.getSuccessors().size() != 1) { + throw new BugInCF( + "loop condition has: " + + blockContainingLoopCondition.getSuccessors().size() + + " successors instead of 1."); + } + Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + if (!(conditionalBlock instanceof ConditionalBlock)) { + throw new BugInCF( + "loop condition successor is not ConditionalBlock, but: " + + conditionalBlock.getClass()); } - } while (changed); - return dom; + loopState.resolvedPotentiallyFulfillingCollectionLoops.add( + new ResolvedPotentiallyFulfillingCollectionLoop( + loop.getExpression(), + loopVariableNode.getTree(), + node.getTree(), + loopBodyEntryBlock, + loopUpdateBlock, + (ConditionalBlock) conditionalBlock, + loopVariableNode)); + loopState.potentiallyFulfillingEnhancedForLoops.remove(loop); + } } - private List findBackEdges(Set blocks, Map> dom) { - List backs = new ArrayList<>(); - for (Block u : blocks) { - for (Block v : u.getSuccessors()) { - if (v == null || !blocks.contains(v)) continue; - Set domU = dom.get(u); - if (domU != null && domU.contains(v)) { - // v dominates u => back edge u -> v - backs.add(new Edge(u, v)); + /** Resolves while-loop candidates into CFG-resolved loops for consistency analysis. */ + private static final class WhileLoopResolver { + /** The CFG of the current method. */ + private final ControlFlowGraph cfg; + + /** + * Creates a resolver for potentially fulfilling while loops in the given CFG. + * + * @param cfg the enclosing method CFG + */ + private WhileLoopResolver(ControlFlowGraph cfg) { + this.cfg = cfg; + } + + /** + * Resolves all potentially fulfilling while loops in the given method state that can be tied to + * a loop update block in the current CFG. + * + * @param loopState per-method collection-loop state + */ + private void resolveWhileLoops(MethodCollectionLoopState loopState) { + if (loopState.potentiallyFulfillingCollectionLoops.isEmpty()) { + return; + } + + WhileLoopResolutionCache whileLoopCache = loopState.getOrCreateWhileLoopCache(cfg); + + Iterator potentialLoopIterator = + loopState.potentiallyFulfillingCollectionLoops.iterator(); + while (potentialLoopIterator.hasNext()) { + PotentiallyFulfillingCollectionLoop potentialLoop = potentialLoopIterator.next(); + ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = + resolveWhileLoop(potentialLoop, whileLoopCache); + if (resolvedLoop != null) { + loopState.resolvedPotentiallyFulfillingCollectionLoops.add(resolvedLoop); + potentialLoopIterator.remove(); } } } - return backs; - } - private Set naturalLoop(Block u, Block v, Set blocks, int budget) { - // Standard natural-loop: start from backedge u->v - Set loop = new HashSet<>(); - ArrayDeque stack = new ArrayDeque<>(); + /** + * Resolves one potentially fulfilling while loop if a suitable loop update block can be found. + * + * @param potentialLoop a potentially fulfilling while loop + * @param whileLoopCache cached CFG facts for while-loop resolution + * @return the CFG-resolved loop, or {@code null} if no loop update block is found + */ + private @Nullable ResolvedPotentiallyFulfillingCollectionLoop resolveWhileLoop( + PotentiallyFulfillingCollectionLoop potentialLoop, + WhileLoopResolutionCache whileLoopCache) { + Block loopUpdateBlock = + chooseLoopUpdateBlockForPotentiallyFulfillingLoop(potentialLoop, whileLoopCache); + if (loopUpdateBlock == null) { + return null; + } + return new ResolvedPotentiallyFulfillingCollectionLoop( + potentialLoop.collectionTree, + potentialLoop.collectionElementTree, + potentialLoop.condition, + potentialLoop.loopBodyEntryBlock, + loopUpdateBlock, + potentialLoop.loopConditionalBlock, + potentialLoop.collectionElementNode); + } + + /** + * Chooses the best loop update block for a potentially fulfilling while loop by matching it to + * the tightest natural loop that contains both the body entry and the loop condition. + * + * @param potentialLoop a potentially fulfilling while loop + * @param whileLoopCache cached CFG facts for while-loop resolution + * @return the chosen loop update block, or {@code null} if none is found + */ + private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( + PotentiallyFulfillingCollectionLoop potentialLoop, + WhileLoopResolutionCache whileLoopCache) { + + Block bodyEntryBlock = potentialLoop.loopBodyEntryBlock; + Block conditionalBlock = potentialLoop.loopConditionalBlock; - loop.add(v); - if (loop.add(u)) stack.push(u); + Block bestLoopUpdateBlock = null; + int bestLoopSize = Integer.MAX_VALUE; - while (!stack.isEmpty() && budget-- > 0) { - Block x = stack.pop(); - for (Block p : x.getPredecessors()) { - if (p == null || !blocks.contains(p)) continue; - if (loop.add(p) && p != v) { - stack.push(p); + for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { + // backEdge.targetBlock is the candidate block that the loop body flows back to. + Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); + + // Must contain this while-loop's body entry and conditional block. + if (!naturalLoop.contains(bodyEntryBlock)) { + continue; + } + if (!naturalLoop.contains(conditionalBlock)) { + continue; + } + + // Prefer the tightest loop. This helps nested-loop disambiguation. + if (naturalLoop.size() < bestLoopSize) { + bestLoopSize = naturalLoop.size(); + bestLoopUpdateBlock = backEdge.targetBlock; } } + + return bestLoopUpdateBlock; } - return loop; } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index eb6f6cc32505..2670ea25ace6 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -60,8 +60,8 @@ public void accumulate( } /** - * Add the collection elements iterated over in potentially Mcoe-obligation-fulfilling loops to - * the store. + * Add the collection elements iterated over in potentially fulfilling collection loops to the + * store. */ @Override public AccumulationStore initialStore( @@ -69,10 +69,22 @@ public AccumulationStore initialStore( AccumulationStore store = super.initialStore(underlyingAST, parameters); RLCCalledMethodsAnnotatedTypeFactory cmAtf = (RLCCalledMethodsAnnotatedTypeFactory) this.analysis.getTypeFactory(); - for (RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingLoop loop : - RLCCalledMethodsAnnotatedTypeFactory.getPotentiallyFulfillingLoops()) { + for (RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingCollectionLoop + potentiallyFulfillingCollectionLoop : + cmAtf.getPotentiallyFulfillingCollectionLoops(underlyingAST)) { IteratedCollectionElement collectionElementJE = - new IteratedCollectionElement(loop.collectionElementNode, loop.collectionElementTree); + new IteratedCollectionElement( + potentiallyFulfillingCollectionLoop.collectionElementNode, + potentiallyFulfillingCollectionLoop.collectionElementTree); + store.insertValue(collectionElementJE, cmAtf.top); + } + for (RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop + resolvedPotentiallyFulfillingLoop : + cmAtf.getResolvedPotentiallyFulfillingCollectionLoops(underlyingAST)) { + IteratedCollectionElement collectionElementJE = + new IteratedCollectionElement( + resolvedPotentiallyFulfillingLoop.collectionElementNode, + resolvedPotentiallyFulfillingLoop.collectionElementTree); store.insertValue(collectionElementJE, cmAtf.top); } return store; @@ -80,7 +92,7 @@ public AccumulationStore initialStore( /** * Accumulates the called methods to this collection element if the given node is the element of a - * collection iterated over in a potentially-Mcoe-obligation-fulfilling loop. + * collection iterated over in a potentially fulfilling collection loop. * * @param valuesAsList the list of called methods * @param result the transfer result diff --git a/checker/tests/resourceleak-collections/AnotherBug.java b/checker/tests/resourceleak-collections/AnotherBug.java deleted file mode 100644 index 468954595b65..000000000000 --- a/checker/tests/resourceleak-collections/AnotherBug.java +++ /dev/null @@ -1,21 +0,0 @@ -import java.util.*; -import org.checkerframework.checker.calledmethods.qual.*; -import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; - -@InheritableMustCall("close") -class OCInCons { - List list; - - public OCInCons(@OwningCollection List list) { - this.list = list; - } - - @CollectionFieldDestructor("this.list") - public void close() { - for (Resource r : list) { - r.close(); - r.flush(); - } - } -} diff --git a/checker/tests/resourceleak-collections/CollectionClearInFinally.java b/checker/tests/resourceleak-collections/CollectionClearInFinally.java new file mode 100644 index 000000000000..58809eda636e --- /dev/null +++ b/checker/tests/resourceleak-collections/CollectionClearInFinally.java @@ -0,0 +1,38 @@ +import java.util.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +/* + * Tests that clearing an owning collection does not interfere with recognizing that each + * element obligation was already discharged. + */ +class CollectionClearInFinally { + /* + * Check that a clear in the finally block is accepted after the loop closes and flushes + * every element. + */ + void clearInFinally(@OwningCollection List resources) { + try { + for (Resource r : resources) { + r.flush(); + r.close(); + } + } finally { + resources.clear(); + } + } + + /* + * Check the same scenario when the collection is cleared after the try/finally statement. + */ + void clearAfterFinally(@OwningCollection List resources) { + try { + for (Resource r : resources) { + r.flush(); + r.close(); + } + } finally { + } + resources.clear(); + } +} diff --git a/checker/tests/resourceleak-collections/CollectionFieldDestructorArgPassing.java b/checker/tests/resourceleak-collections/CollectionFieldDestructorArgPassing.java new file mode 100644 index 000000000000..933851c79d7b --- /dev/null +++ b/checker/tests/resourceleak-collections/CollectionFieldDestructorArgPassing.java @@ -0,0 +1,113 @@ +import java.io.IOException; +import java.util.*; +import java.util.Iterator; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +/* + * Tests collection field destructors that pass the owning collection to helpers or discharge + * it through a while loop. + */ +class CollectionFieldDestructorArgPassing { + + @InheritableMustCall("close") + static class ThrowingCloseResource { + void close() throws IOException {} + } + + @InheritableMustCall("shutdownConnections") + static class Worker { + + final @OwningCollection List tcpConnections = new ArrayList<>(); + + @CreatesMustCallFor("this") + void add(@Owning ThrowingCloseResource resource) { + tcpConnections.add(resource); + } + + @CollectionFieldDestructor("this.tcpConnections") + /* + * Delegating the field to another method should not satisfy the field destructor + * contract, because it attempts to transfer ownership away from the field. + */ + // ::error: contracts.postcondition + void shutdownConnections() throws IOException { + // ::error: transfer.owningcollection.field.ownership + fieldCloser(this.tcpConnections); + } + + void fieldCloser(@OwningCollection List tcpConnections) { + for (ThrowingCloseResource r : tcpConnections) { + try { + r.close(); + } catch (IOException e) { + } + } + } + } + + @InheritableMustCall("shutdownConnections") + static class Worker2 { + + final @OwningCollection List tcpConnections = new ArrayList<>(); + + @CreatesMustCallFor("this") + void add(@Owning ThrowingCloseResource resource) { + tcpConnections.add(resource); + } + + @CollectionFieldDestructor("this.tcpConnections") + /* + * A while loop that iterates the field directly should satisfy the field destructor + * contract. + */ + void shutdownConnections() throws IOException { + Iterator it = tcpConnections.iterator(); + while (it.hasNext()) { + ThrowingCloseResource r = it.next(); + try { + r.close(); + } catch (IOException e) { + } + } + } + } + + static void checkArgIsOCWO( + // :: error: illegal.type.annotation + @OwningCollectionWithoutObligation Iterable arg) {} + + /* + * Check that the helper-based destructor still leaves the wrapper object with an + * outstanding must-call obligation. + */ + void client() throws Exception { + Worker w = new Worker(); + w.add(new ThrowingCloseResource()); + w.shutdownConnections(); + } + + /* + * Check the same helper-based case when the wrapper is used under ordinary exception + * handling. + */ + void client2() { + Worker w = new Worker(); + w.add(new ThrowingCloseResource()); + try { + w.shutdownConnections(); + } catch (IOException e) { + } + } + + /* + * Sanity-check the resource type itself outside of any collection obligations. + */ + void client3() { + ThrowingCloseResource r = new ThrowingCloseResource(); + try { + r.close(); + } catch (IOException e) { + } + } +} diff --git a/checker/tests/resourceleak-collections/FinallyBug.java b/checker/tests/resourceleak-collections/FinallyBug.java deleted file mode 100644 index adf4e20d8a50..000000000000 --- a/checker/tests/resourceleak-collections/FinallyBug.java +++ /dev/null @@ -1,28 +0,0 @@ -import java.util.*; -import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; - -class FinallyBug { - void f(@OwningCollection List col) { - try { - for (Resource r : col) { - r.flush(); - r.close(); - } - } finally { - col.clear(); - } - } - - void f2(@OwningCollection List col) { - try { - for (Resource r : col) { - r.flush(); - r.close(); - } - } finally { - - } - col.clear(); - } -} diff --git a/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java b/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java index a86784f642cd..0b9a3a0f679e 100644 --- a/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java +++ b/checker/tests/resourceleak-collections/InfiniteLoopGrowingCollection.java @@ -1,85 +1,37 @@ -// import java.io.IOException; -// import java.nio.channels.SocketChannel; -// import java.util.ArrayList; -// import java.util.List; -// -// import org.checkerframework.checker.calledmethods.qual.*; -// import org.checkerframework.checker.collectionownership.qual.*; -// import org.checkerframework.checker.mustcall.qual.*; -// -// class InfiniteLoopGrowingCollection { -// -// void serverLoop() { -// @OwningCollection List socketChannelList = new ArrayList<>(); -// -// while (true) { -// SocketChannel sc = null; -// try { -// // Something that creates an owning resource repeatedly -// sc = SocketChannel.open(); -// sc.configureBlocking(false); -// } catch (IOException e) { -// // swallow: no checked exceptional exit from the method -// } -// -// if (sc != null) { -// // :: error: collection.obligation.never.enforced -// socketChannelList.add(sc); -// } -// } -// } -// } - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; +import java.io.Closeable; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; -import org.checkerframework.checker.calledmethods.qual.*; import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.nullness.qual.Nullable; +/* + * Tests the normal-exit check for growing an owning collection inside a loop that never + * reaches a regular exit. + */ class InfiniteLoopGrowingCollection { - public static void main(String[] args) throws IOException { - ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); - serverSocketChannel.bind(new InetSocketAddress(8080)); - @OwningCollection List socketChannelList = new ArrayList<>(); - byte[] bytes = new byte[1024]; - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - serverSocketChannel.configureBlocking(false); + @InheritableMustCall("close") + static class LoopResource implements Closeable { + @Override + public void close() {} + } + + @Nullable LoopResource maybeAccept() { + return null; + } + + /* + * Adding a resource to the collection inside an infinite loop should report that the + * obligation can never be enforced at a regular exit. + */ + void serverLoop() { + @OwningCollection List resources = new ArrayList<>(); while (true) { - SocketChannel socketChannel = serverSocketChannel.accept(); - if (socketChannel == null) { - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("无人连接"); - for (SocketChannel item : socketChannelList) { - int len = item.read(byteBuffer); - if (len > 0) { - byteBuffer.flip(); - System.out.println("读取到的数据" + new String(byteBuffer.array(), 0, len)); - } - byteBuffer.clear(); - } - } else { - socketChannel.configureBlocking(false); + LoopResource resource = maybeAccept(); + if (resource != null) { // :: error: collection.obligation.never.enforced - socketChannelList.add(socketChannel); - for (SocketChannel item : socketChannelList) { - int len = item.read(byteBuffer); - if (len > 0) { - byteBuffer.flip(); - System.out.println("读取到的数据" + new String(byteBuffer.array(), 0, len)); - } - byteBuffer.clear(); - } + resources.add(resource); } } } diff --git a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java index 0e8316759f38..2f8fba356e2c 100644 --- a/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java +++ b/checker/tests/resourceleak-collections/LoopBodyAnalysisTest.java @@ -5,7 +5,7 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -class LoopBodyAnalysisTests { +class LoopBodyAnalysisTest { void fullSatisfyCollection(@OwningCollection Collection resources) { for (Resource r : resources) { diff --git a/checker/tests/resourceleak-collections/MissingAnnotations.java b/checker/tests/resourceleak-collections/MissingAnnotations.java deleted file mode 100644 index a02b944974f3..000000000000 --- a/checker/tests/resourceleak-collections/MissingAnnotations.java +++ /dev/null @@ -1,51 +0,0 @@ -import java.io.*; -import java.nio.channels.*; -import java.util.*; -import org.checkerframework.checker.calledmethods.qual.*; -import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; - -class SetTest { - // :: error: unfulfilled.field.obligations - Set resSet = new HashSet<>(); - - private InputStream[] responseSequenceBinary(List fileNames) throws IOException { - List response = new ArrayList<>(); - for (var fileName : fileNames) { - // :: error: unfulfilled.collection.obligations - response.add(new FileInputStream(fileName)); - } - return response.toArray(new InputStream[0]); - } -} -// -// @InheritableMustCall("close") -// class DestructorWithThis { -// private final Queue files; -// -// DestructorWithThis(int size) { -// this.files = new ConcurrentLinkedQueue(); -// } -// -// @CreatesMustCallFor("this") -// public void release(@Owning RandomAccessFile file) { -// this.files.add(file); -// } -// -// @CollectionFieldDestructor("this.files") -// public void close() throws IOException { -// try { -// while (!this.files.isEmpty()) { -// RandomAccessFile pooledFile = this.files.poll(); -// if (pooledFile != null) { -// try { -// pooledFile.close(); -// } catch (IOException e) { -// } -// } -// } -// } finally { -// this.files.clear(); -// } -// } -// } diff --git a/checker/tests/resourceleak-collections/MissingCollectionOwnershipAnnotations.java b/checker/tests/resourceleak-collections/MissingCollectionOwnershipAnnotations.java new file mode 100644 index 000000000000..794ef68ddc38 --- /dev/null +++ b/checker/tests/resourceleak-collections/MissingCollectionOwnershipAnnotations.java @@ -0,0 +1,27 @@ +import java.io.*; +import java.util.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.collectionownership.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +/* + * Tests that omitted ownership annotations still lead to the expected field and collection + * obligation errors. + */ +class MissingCollectionOwnershipAnnotations { + // :: error: unfulfilled.field.obligations + Set resSet = new HashSet<>(); + + /* + * Building a local collection of streams without discharging or transferring ownership + * should report an unfulfilled collection obligation. + */ + private InputStream[] responseSequenceBinary(List fileNames) throws IOException { + List response = new ArrayList<>(); + for (var fileName : fileNames) { + // :: error: unfulfilled.collection.obligations + response.add(new FileInputStream(fileName)); + } + return response.toArray(new InputStream[0]); + } +} diff --git a/checker/tests/resourceleak-collections/MultipleInputStream.java b/checker/tests/resourceleak-collections/MultipleInputStream.java index b885448f7828..b03129cba5a4 100644 --- a/checker/tests/resourceleak-collections/MultipleInputStream.java +++ b/checker/tests/resourceleak-collections/MultipleInputStream.java @@ -1,13 +1,15 @@ import java.io.IOException; import java.io.InputStream; -import java.util.*; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; +import java.util.List; import org.checkerframework.checker.calledmethods.qual.*; import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; +// Tests constructor paths that transfer resource collections through List.addAll. +// TODO: Fix arguemnt errors for List.addAll call. public class MultipleInputStream extends InputStream { private final List streams = new LinkedList<>(); @@ -16,10 +18,12 @@ public class MultipleInputStream extends InputStream { private int currentIndex; + // ::error: unfulfilled.collection.obligations public MultipleInputStream(@OwningCollection Collection streams) { if (streams.size() == 0) { throw new IllegalArgumentException("At least one stream is required"); } + // ::error: argument this.streams.addAll(streams); incrementCurrent(); } @@ -28,6 +32,7 @@ public MultipleInputStream(InputStream... streams) { if (streams.length == 0) { throw new IllegalArgumentException("At least one stream is required"); } + // ::error: argument this.streams.addAll(Arrays.asList(streams)); incrementCurrent(); } @@ -44,17 +49,13 @@ private boolean incrementCurrent() { return true; } - @NotOwning - Resource nonOwn() { - return null; - } - - Resource[] test() { - - @NotOwningCollection List col = new ArrayList<>(); - col.add(nonOwn()); - Resource[] streamsWithField = new Resource[col.size()]; - streamsWithField = col.toArray(streamsWithField); - return streamsWithField; + @CollectionFieldDestructor("this.streams") + public void close() throws IOException { + for (InputStream i : streams) { + try { + i.close(); + } catch (IOException e) { + } + } } } diff --git a/checker/tests/resourceleak-collections/NotOwningLocal.java b/checker/tests/resourceleak-collections/NotOwningCollectionLocal.java similarity index 57% rename from checker/tests/resourceleak-collections/NotOwningLocal.java rename to checker/tests/resourceleak-collections/NotOwningCollectionLocal.java index 84d35ada1669..13d24e3738aa 100644 --- a/checker/tests/resourceleak-collections/NotOwningLocal.java +++ b/checker/tests/resourceleak-collections/NotOwningCollectionLocal.java @@ -4,16 +4,20 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -class NotOwningLocal { +/* + * Tests that a local explicitly typed as @NotOwningCollection retains that type through + * mutation and iterator creation. + */ +class NotOwningCollectionLocal { @NotOwning - InputStream get() { + InputStream newNonOwningStream() { throw new Error(); } @NotOwningCollection - Iterator test() { + Iterator iteratorFromNotOwningLocal() { @NotOwningCollection List list = new ArrayList<>(); - list.add(get()); + list.add(newNonOwningStream()); return list.iterator(); } } diff --git a/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java index 1135f27e7670..13a1b12e2a13 100644 --- a/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java +++ b/checker/tests/resourceleak-collections/NotOwningCollectionMutationTest.java @@ -3,219 +3,209 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; +/* + * Tests mutation policy for non-owning and owning collections. + * + *

The non-owning cases check that only definitely non-owning elements may be inserted. + * The owning cases check that inserted obligations are tracked and later discharged. + */ class NotOwningCollectionMutationTest { @InheritableMustCall("close") - static class R { + static class TrackedResource { void close() throws IOException {} } - R owning() { - return new R(); + TrackedResource owning() { + return new TrackedResource(); } @NotOwning - R notowning() { + TrackedResource notOwning() { // :: error: required.method.not.called - return new R(); + return new TrackedResource(); } - // ------------------------------------------------------------ - // NotOwningCollection receiver + NotOwning element: OK - // ------------------------------------------------------------ - void ok_noc_add_notowning_param(@NotOwningCollection List l, @NotOwning R r) { - l.add(r); // ok + /* + * Non-owning collection receivers may accept only definitely non-owning elements. + */ + void okNocAddNotOwningParam( + @NotOwningCollection List list, @NotOwning TrackedResource resource) { + list.add(resource); } - void ok_noc_add_notowning_method(@NotOwningCollection List l) { - l.add(notowning()); // ok + void okNocAddNotOwningMethod(@NotOwningCollection List list) { + list.add(notOwning()); } - // ------------------------------------------------------------ - // NotOwningCollection receiver + owning element: error (smuggling) - // Close afterwards to avoid extra RLC "required.method.not.called". - // ------------------------------------------------------------ - void err_noc_add_owning_local_then_close(@NotOwningCollection List l) { - R r = new R(); + /* + * Inserting owning elements into a non-owning collection is illegal even if the caller + * closes the value afterward. + */ + void errNocAddOwningLocalThenClose(@NotOwningCollection List list) { + TrackedResource resource = new TrackedResource(); // :: error: illegal.collection.mutator.owning.insert.into.notowning - l.add(r); + list.add(resource); try { - r.close(); + resource.close(); } catch (IOException e) { // ignore } } - void err_noc_add_owning_param_then_close(@NotOwningCollection List l, @Owning R r) { + void errNocAddOwningParamThenClose( + @NotOwningCollection List list, @Owning TrackedResource resource) { // :: error: illegal.collection.mutator.owning.insert.into.notowning - l.add(r); + list.add(resource); try { - r.close(); + resource.close(); } catch (IOException e) { // ignore } } - void err_noc_add_alias_of_owning_then_close(@NotOwningCollection List l) { - R r = new R(); - R r2 = r; + void errNocAddAliasOfOwningThenClose(@NotOwningCollection List list) { + TrackedResource resource = new TrackedResource(); + TrackedResource alias = resource; // :: error: illegal.collection.mutator.owning.insert.into.notowning - l.add(r2); + list.add(alias); try { - r.close(); + resource.close(); } catch (IOException e) { // ignore } } - // ------------------------------------------------------------ - // NotOwningCollection receiver via move/alias: old name becomes NOC - // (assuming your assignment transfer sets RHS (old owner) to NOC) - // ------------------------------------------------------------ - void err_noc_receiver_after_move_then_close() { - @OwningCollection List owner = new ArrayList<>(); - List newOwner = owner; // after transfer: "owner" becomes @NotOwningCollection - R r = new R(); + /* + * Reassignment turns the old owner name into a non-owning alias. Inserting through that + * old name should therefore be rejected. + */ + void errNocReceiverAfterMoveThenClose() { + @OwningCollection List owner = new ArrayList<>(); + List newOwner = owner; + TrackedResource resource = new TrackedResource(); // :: error: illegal.collection.mutator.owning.insert.into.notowning - owner.add(r); + owner.add(resource); try { - r.close(); + resource.close(); } catch (IOException e) { // ignore } - for (R r2 : newOwner) { + for (TrackedResource item : newOwner) { try { - r2.close(); - } catch (Exception e) { + item.close(); + } catch (IOException e) { + // ignore } } } - // ------------------------------------------------------------ - // NotOwningCollection as a field: same rule (can add only non-owning) - // ------------------------------------------------------------ - - final @NotOwningCollection List cache = new ArrayList<>(); + final @NotOwningCollection List cache = new ArrayList<>(); - void ok_noc_field_add_notowning(@NotOwning R r) { - cache.add(r); // ok + /* + * Non-owning collection fields follow the same mutation rule as local non-owning + * collection references. + */ + void okNocFieldAddNotOwning(@NotOwning TrackedResource resource) { + cache.add(resource); } - void ok_noc_field_add_notowning_2(R r) { - cache.add(r); // also ok + void okNocFieldAddDefaultParam(TrackedResource resource) { + cache.add(resource); } - void err_noc_field_add_owning_then_close() { - R r = new R(); + void errNocFieldAddOwningThenClose() { + TrackedResource resource = new TrackedResource(); // :: error: illegal.collection.mutator.owning.insert.into.notowning - cache.add(r); + cache.add(resource); try { - r.close(); + resource.close(); } catch (IOException e) { // ignore } } - // ------------------------------------------------------------ - // OwningCollection receiver + owning element: should create collection obligation. - // If you don't discharge via a certified loop, expect unfulfilled.collection.obligations. - // ------------------------------------------------------------ - - void err_oc_add_owning_local_no_dispose() { - @OwningCollection List l = new ArrayList<>(); - R r = new R(); + /* + * Owning collections may accumulate element obligations, which must later be discharged + * by a certified loop. + */ + void errOcAddOwningLocalNoDispose() { + @OwningCollection List list = new ArrayList<>(); + TrackedResource resource = new TrackedResource(); // :: error: unfulfilled.collection.obligations - l.add(r); + list.add(resource); } - void err_oc_add_owning_call_no_dispose() { - List l = new ArrayList<>(); + void errOcAddOwningCallNoDispose() { + List list = new ArrayList<>(); // :: error: unfulfilled.collection.obligations - l.add(owning()); + list.add(owning()); } - void err_oc_add_notowning_call_no_dispose() { - @OwningCollection List l = new ArrayList<>(); - // :: error: illegal.collection.mutator.nonowning.insert.into.owning + void errOcAddNotOwningCallNoDispose() { + @OwningCollection List list = new ArrayList<>(); // :: error: unfulfilled.collection.obligations - l.add(notowning()); + list.add(notOwning()); } - void err_oc_add_owning_alias_no_dispose() { - @OwningCollection List l = new ArrayList<>(); - R r = new R(); - R r2 = r; + void errOcAddOwningAliasNoDispose() { + @OwningCollection List list = new ArrayList<>(); + TrackedResource resource = new TrackedResource(); + TrackedResource alias = resource; // :: error: unfulfilled.collection.obligations - l.add(r2); + list.add(alias); } - void err_oc_add_owning_newexpr_no_dispose() { - @OwningCollection List l = new ArrayList<>(); + void errOcAddOwningNewExprNoDispose() { + @OwningCollection List list = new ArrayList<>(); // :: error: unfulfilled.collection.obligations - l.add(new R()); + list.add(new TrackedResource()); } - // Discharge via a certified loop (close is inside try/catch so no early-exit via exception). - void ok_oc_add_owning_then_dispose_loop() { - @OwningCollection List l = new ArrayList<>(); - R r = new R(); - l.add(r); + void okOcAddOwningThenDisposeLoop() { + @OwningCollection List list = new ArrayList<>(); + TrackedResource resource = new TrackedResource(); + list.add(resource); - for (R x : l) { + for (TrackedResource item : list) { try { - x.close(); + item.close(); } catch (IOException e) { - + // ignore } } } - void ok_oc_add_two_then_dispose_loop() { - @OwningCollection List l = new ArrayList<>(); - R r1 = new R(); - R r2 = new R(); - l.add(r1); - l.add(r2); + void okOcAddTwoThenDisposeLoop() { + @OwningCollection List list = new ArrayList<>(); + TrackedResource first = new TrackedResource(); + TrackedResource second = new TrackedResource(); + list.add(first); + list.add(second); - for (R x : l) { + for (TrackedResource item : list) { try { - x.close(); + item.close(); } catch (IOException e) { - // swallow + // ignore } } } - // inserting @NotOwning elements into an owning collection: - void err_oc_insert_notowning(@OwningCollection List l, @NotOwning R r) { - // :: error: illegal.collection.mutator.nonowning.insert.into.owning - l.add(r); - close_collection(l); - } - - void close_collection(@OwningCollection List l) { - for (R r : l) { + /* + * Inserting non-owning elements into an owning collection is allowed, because it does not + * create a new collection obligation. + */ + void okOcInsertNotOwning( + @OwningCollection List list, @NotOwning TrackedResource resource) { + list.add(resource); + for (TrackedResource item : list) { try { - r.close(); + item.close(); } catch (IOException e) { + // ignore } } } - - // ------------------------------------------------------------ - // addAll: TODO: Take care of the type errors in addAll via JDK stubs - // ------------------------------------------------------------ - - // void err_noc_receiver_addAll_noc_arg( - // @NotOwningCollection List dst, @NotOwningCollection List src) { - // // :: error: method.invocation - // dst.addAll(src); - // } - // - // void err_noc_receiver_addAll_oc_arg( - // @NotOwningCollection List dst, @OwningCollection List src) { - // // :: error: method.invocation - // dst.addAll(src); - // } } diff --git a/checker/tests/resourceleak-collections/GraphFoundationCrash.java b/checker/tests/resourceleak-collections/PoolPruneCrash.java similarity index 74% rename from checker/tests/resourceleak-collections/GraphFoundationCrash.java rename to checker/tests/resourceleak-collections/PoolPruneCrash.java index 85256ebb18e3..16cd0cd91df5 100644 --- a/checker/tests/resourceleak-collections/GraphFoundationCrash.java +++ b/checker/tests/resourceleak-collections/PoolPruneCrash.java @@ -4,13 +4,16 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; +/* + * Crash reproducer for pruning a pooled owning collection after it was initialized with an + * ArrayList(int) constructor. + */ @InheritableMustCall("close") -class CrashRepro { +class PoolPruneCrash { - // TODO: Bug ArrayList l = new ArrayList<>() has JDK issues. private List pool; - CrashRepro(int maxSize) { + PoolPruneCrash(int maxSize) { this.pool = new ArrayList<>(maxSize); // should trigger the crash path } @@ -25,9 +28,9 @@ void close() { } public synchronized void prune() { - // if (pool == null) { - // return; - // } + if (pool == null) { + return; + } Iterator itr = pool.iterator(); while (itr.hasNext()) { Resource reader = itr.next(); diff --git a/checker/tests/resourceleak-collections/Buggz.java b/checker/tests/resourceleak-collections/SocketUtilThrowableFallback.java similarity index 91% rename from checker/tests/resourceleak-collections/Buggz.java rename to checker/tests/resourceleak-collections/SocketUtilThrowableFallback.java index 8463b181dbea..df0de1488a2d 100644 --- a/checker/tests/resourceleak-collections/Buggz.java +++ b/checker/tests/resourceleak-collections/SocketUtilThrowableFallback.java @@ -8,7 +8,11 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -class SocketUtil { +/* + * Reproducer for socket allocation code that falls back through Throwable-based recovery + * while still closing any sockets that were successfully created. + */ +class SocketUtilThrowableFallback { public static Integer[] findUnusedLocalPorts(final int ports) { Throwable firstFoundExc = null; diff --git a/checker/tests/resourceleak-collections/Test.java b/checker/tests/resourceleak-collections/Test.java deleted file mode 100644 index 8c7e62c5d1be..000000000000 --- a/checker/tests/resourceleak-collections/Test.java +++ /dev/null @@ -1,94 +0,0 @@ -import java.io.IOException; -import java.util.*; -import java.util.Iterator; -import org.checkerframework.checker.collectionownership.qual.*; -import org.checkerframework.checker.mustcall.qual.*; - -class CollectionFieldDestructorExceptionPathTest { - - @InheritableMustCall("close") - static class Resource2 { - void close() throws IOException {} - } - - @InheritableMustCall("shutdownConnections") - static class Worker { - - final @OwningCollection List tcpConnections = new ArrayList<>(); - - @CreatesMustCallFor("this") - void add(@Owning Resource2 resource) { - tcpConnections.add(resource); - } - - @CollectionFieldDestructor("this.tcpConnections") - // ::error: contracts.postcondition - void shutdownConnections() throws IOException { - fieldCloser(this.tcpConnections); - } - - void fieldCloser(@OwningCollection List tcpConnections) { - for (Resource2 r : tcpConnections) { - try { - r.close(); - } catch (IOException e) { - } - } - } - - void test() { - List ll = new ArrayList<>(); - ll.add(new Resource2()); - ll = this.tcpConnections; - } - } - - @InheritableMustCall("shutdownConnections") - static class Worker2 { - - final @OwningCollection List tcpConnections = new ArrayList<>(); - - @CreatesMustCallFor("this") - void add(@Owning Resource2 resource) { - tcpConnections.add(resource); - } - - @CollectionFieldDestructor("this.tcpConnections") - void shutdownConnections() throws IOException { - Iterator it = tcpConnections.iterator(); - while (it.hasNext()) { - Resource2 r = it.next(); - try { - r.close(); - } catch (IOException e) { - } - } - } - } - - // :: error: illegal.type.annotation - static void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} - - void client() throws Exception { - Worker w = new Worker(); - w.add(new Resource2()); - w.shutdownConnections(); - } - - void client2() { - Worker w = new Worker(); - w.add(new Resource2()); - try { - w.shutdownConnections(); - } catch (IOException e) { - } - } - - void client3() { - Resource2 r = new Resource2(); - try { - r.close(); - } catch (IOException e) { - } - } -} diff --git a/checker/tests/resourceleak-collections/Bug.java b/checker/tests/resourceleak-collections/TokenCacheFileSystemCleanup.java similarity index 91% rename from checker/tests/resourceleak-collections/Bug.java rename to checker/tests/resourceleak-collections/TokenCacheFileSystemCleanup.java index 0c263c8fb98a..ac4da241f00f 100644 --- a/checker/tests/resourceleak-collections/Bug.java +++ b/checker/tests/resourceleak-collections/TokenCacheFileSystemCleanup.java @@ -36,7 +36,11 @@ public FileSystem getFileSystem() { } } -class TokenCache { +/* + * Reproducer for collecting FileSystem instances, using them, and closing them in a finally + * block. + */ +class TokenCacheFileSystemCleanup { private static final Log LOG = null; diff --git a/checker/tests/resourceleak-collections/WhileLoopDestructor.java b/checker/tests/resourceleak-collections/WhileLoopBodyAnalysisTest.java similarity index 76% rename from checker/tests/resourceleak-collections/WhileLoopDestructor.java rename to checker/tests/resourceleak-collections/WhileLoopBodyAnalysisTest.java index 4675fdc56a84..e1e3dc5d6457 100644 --- a/checker/tests/resourceleak-collections/WhileLoopDestructor.java +++ b/checker/tests/resourceleak-collections/WhileLoopBodyAnalysisTest.java @@ -1,7 +1,4 @@ import java.io.*; -import java.io.Closeable; -import java.util.*; -import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -11,11 +8,15 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -class LoopBodyAnalysisWhileTests { +/* + * Tests certification of collection obligations for supported while-loop patterns. + */ +class WhileLoopBodyAnalysisTest { - // ------------------------- - // WHILE LOOP: Iterator.hasNext/next - // ------------------------- + /* + * Iterator.hasNext()/next() loops can certify collection obligations when every element is + * fully satisfied. + */ void whileIteratorFull(@OwningCollection Collection resources) { Iterator it = resources.iterator(); while (it.hasNext()) { @@ -38,6 +39,10 @@ void whileIteratorFullNullCheck(@OwningCollection Collection resources checkArgIsOCWO(resources); } + /* + * Early exit prevents certification of the loop body, even if the loop closes the current + * element before breaking. + */ // :: error: unfulfilled.collection.obligations void whileIteratorFullEarlyBreak(@OwningCollection Collection resources) { Iterator it = resources.iterator(); @@ -53,9 +58,9 @@ void whileIteratorFullEarlyBreak(@OwningCollection Collection resource checkArgIsOCWO(resources); } - // ------------------------- - // WHILE LOOP: Queue.isEmpty/poll - // ------------------------- + /* + * Queue.isEmpty()/poll() loops are also supported. + */ void whileQueuePollFull(@OwningCollection Queue resources) { while (!resources.isEmpty()) { Resource r = resources.poll(); @@ -67,9 +72,9 @@ void whileQueuePollFull(@OwningCollection Queue resources) { checkArgIsOCWO(resources); } - // ------------------------- - // WHILE LOOP: Stack.isEmpty/pop (FULL) - // ------------------------- + /* + * Stack loops should work for both isEmpty()/pop() and size()/pop(). + */ void whileStackPopFull(@OwningCollection Stack resources) { while (!resources.isEmpty()) { Resource r = resources.pop(); @@ -88,10 +93,9 @@ void whileStackPopFullWithSize(@OwningCollection Stack resources) { checkArgIsOCWO(resources); } - // ------------------------- - // NEGATIVE: Iterator while-loop missing flush - // ------------------------- - + /* + * Missing one of the required methods should prevent loop certification. + */ // :: error: unfulfilled.collection.obligations void whileIteratorPartialShouldError(@OwningCollection List resources) { Iterator it = resources.iterator(); @@ -104,6 +108,10 @@ void whileIteratorPartialShouldError(@OwningCollection List resources) checkArgIsOCWO(resources); } + /* + * The same partial-satisfaction case should fail for close-only resources that throw + * checked exceptions. + */ void whileIteratorPartialShouldError2(@OwningCollection List resources) throws IOException { Iterator it = resources.iterator(); @@ -117,27 +125,9 @@ void whileIteratorPartialShouldError2(@OwningCollection List re checkArgIsOCWO2(resources); } - // Helper used by the tests (same as your for-loop file). // :: error: illegal.type.annotation void checkArgIsOCWO(@OwningCollectionWithoutObligation Iterable arg) {} // :: error: illegal.type.annotation void checkArgIsOCWO2(@OwningCollectionWithoutObligation Iterable arg) {} } - -abstract class RLCCollections { - - abstract Closeable alloc(); - - void foo() throws Exception { - @OwningCollection Collection resources = new ArrayList(); - resources.add(alloc()); - - for (var r : resources) { - try { - r.close(); - } catch (IOException e) { - } - } - } -} diff --git a/checker/tests/resourceleak/IndexMode.java b/checker/tests/resourceleak/IndexMode.java index abd9f0937217..db860e2c6dc8 100644 --- a/checker/tests/resourceleak/IndexMode.java +++ b/checker/tests/resourceleak/IndexMode.java @@ -38,9 +38,9 @@ public static Object getMode2(Map indexOpt // This copy of getMode() adds an explicit `@MustCall` annotation to the String and to // the local variable. This version currently works as expected, unlike getMode2(). - // Since Map#get returns @NotOwning, this reports no error. public static Object getMode2a(Map indexOptions) { try { + // :: error: required.method.not.called @MustCall("hashCode") String literalOption = indexOptions.get("is_literal"); } catch (Exception e) { } @@ -51,7 +51,7 @@ public static Object getMode2a(Map indexOp // This copy of getMode() adds an explicit `@MustCall` annotation to the String and removes // the try-catch. public static Object getMode3(Map indexOptions) { - // Since Map#get returns @NotOwning, this reports no error. + // :: error: required.method.not.called String literalOption = indexOptions.get("is_literal"); return null; } @@ -59,7 +59,7 @@ public static Object getMode3(Map indexOpt // This copy of getMode() adds an explicit `@MustCall` annotation to the String, removes // the try-catch, and makes the return type void. public static void getMode4(Map indexOptions) { - // Since Map#get returns @NotOwning, this reports no error. + // :: error: required.method.not.called String literalOption = indexOptions.get("is_literal"); } @@ -72,7 +72,7 @@ public static void getMode5(Map indexOptions) { // value type in the map. The values are permitted to have any @MustCall type. public static Object getModeIS(Map indexOptions) { try { - // Since Map#get returns @NotOwning, this reports no error. + // :: error: required.method.not.called InputStream literalOption = indexOptions.get("is_literal"); } catch (Exception e) { } diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java index 7032dbc2bcdc..532c38eebab5 100644 --- a/checker/tests/resourceleak/Issue4815.java +++ b/checker/tests/resourceleak/Issue4815.java @@ -13,7 +13,6 @@ public void initialize( // type checker. This is an unfortunate consequence of the otherwise // elegant extension of the RLC to collections, which doesn't detect // that object already fulfilled its obligation here. - // :: error: argument list.add(object); } diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java index 971f98cb7d2a..79d96b7663bc 100644 --- a/checker/tests/resourceleak/SocketIntoList.java +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -26,10 +26,11 @@ public void test3(List<@MustCall({}) Socket> l) throws Exception { // Although s is illegally added to l, a required.method.not.called error // is not additionally reported at this declaration site. List#add(@Owning E) takes on // the obligation of its argument. + // After RLCC, now we get the expected required.method.not.called error. + // :: error: required.method.not.called Socket s = new Socket(); s.bind(new InetSocketAddress("192.168.0.1", 0)); // l cannot hold elements with non-empty @MustCall values - // :: error: argument l.add(s); } From 26f842240504d40d51b007c0c01c20f58b8331fc Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Wed, 1 Apr 2026 12:14:16 -0700 Subject: [PATCH 348/374] Documentation in COATF and COA. --- .../CollectionOwnershipAnalysis.java | 31 ++++++++---- ...llectionOwnershipAnnotatedTypeFactory.java | 50 +++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index 8cf8220cb464..ef63869c98b7 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -21,9 +21,10 @@ public class CollectionOwnershipAnalysis extends CFAbstractAnalysis { /** - * The resource-leak ignored-exception policy, except that RLCC does not ignore {@link Throwable}. - * Broad {@code Throwable}-only exceptional paths affect collection-ownership flow in ways that - * matter for RLCC, so this analysis treats them as real exceptional control flow. + * Ignored-exception policy used by collection-ownership flow. + * + *

This policy matches the Resource Leak Checker policy except that exact {@link Throwable} + * exceptional edges are preserved. */ private final SetOfTypes ignoredExceptions; @@ -31,19 +32,31 @@ public class CollectionOwnershipAnalysis * Creates a new {@link CollectionOwnershipAnalysis}. * * @param checker the checker - * @param factory the factory + * @param factory the collection-ownership type factory */ public CollectionOwnershipAnalysis( BaseTypeChecker checker, CollectionOwnershipAnnotatedTypeFactory factory) { super(checker, factory); + ignoredExceptions = createIgnoredExceptionPolicy(checker); + } + + /** + * Creates the ignored-exception policy used by collection-ownership flow. + * + *

The policy follows the Resource Leak Checker configuration, but keeps exact{@link Throwable} + * exceptional edges because broad catch/fallback paths affect collection-obligation reasoning. + * + * @param checker the enclosing checker + * @return the ignored-exception policy for collection-ownership flow + */ + private static SetOfTypes createIgnoredExceptionPolicy(BaseTypeChecker checker) { ResourceLeakChecker resourceLeakChecker = ResourceLeakUtils.getResourceLeakChecker(checker); SetOfTypes baseIgnoredExceptions = resourceLeakChecker.getIgnoredExceptions(); - SetOfTypes exactThrowable = + SetOfTypes exactThrowableOnly = SetOfTypes.anyOfTheseNames(ImmutableSet.of(Throwable.class.getCanonicalName())); - ignoredExceptions = - (types, exceptionType) -> - !exactThrowable.contains(types, exceptionType) - && baseIgnoredExceptions.contains(types, exceptionType); + return (types, exceptionType) -> + !exactThrowableOnly.contains(types, exceptionType) + && baseIgnoredExceptions.contains(types, exceptionType); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 739c91d83312..1bbcc9600d46 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -27,6 +27,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; +import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollectionBottom; @@ -108,8 +109,8 @@ public class CollectionOwnershipAnnotatedTypeFactory TreeUtils.getMethod(CollectionFieldDestructor.class, "value", 0, processingEnv); /** - * Method CFGs whose resource-leak post-analysis already ran before contained lambdas were - * analyzed. + * Method CFGs whose resource-leak post-analysis already ran after the first method analysis and + * before any contained lambdas were analyzed. */ private final Set preLambdaPostAnalyzedMethods = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -248,26 +249,45 @@ public CollectionOwnershipStore getStoreForBlock( : flowResult.getStoreBefore(succBlock); } - // Note that this method is overridden here because the collections ownership - // typechecker runs last in the RLC, not because this has anything to do with - // collections. Whatever checker runs last in the RLC must do this. TODO: make this - // run last in a more sensible way. + /** + * Runs resource-leak post-analysis after the first method analysis and before any contained + * lambdas are analyzed. + * + *

This override exists because the Collection Ownership Checker currently runs last in the + * Resource Leak Checker hierarchy. The last checker in that hierarchy is responsible for + * triggering the final resource-leak post-analysis for the method. + * + * @param cfg the method CFG that has completed its first analysis + */ @Override protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { runResourceLeakPostAnalyze(cfg); preLambdaPostAnalyzedMethods.add(cfg); } + /** + * Performs post-analysis for the given CFG. + * + *

If resource-leak-specific post-analysis already ran during {@link + * #postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph)}, then this method avoids running it a + * second time. + * + * @param cfg the CFG to post-analyze + */ @Override public void postAnalyze(ControlFlowGraph cfg) { if (!preLambdaPostAnalyzedMethods.remove(cfg)) { runResourceLeakPostAnalyze(cfg); } - super.postAnalyze(cfg); } - /** Runs the resource-leak-specific post-analysis that must happen in the last checker. */ + /** + * Runs the resource-leak post-analysis that must happen in the last checker in the Resource Leak + * Checker hierarchy. + * + * @param cfg the CFG to analyze + */ private void runResourceLeakPostAnalyze(ControlFlowGraph cfg) { ResourceLeakChecker rlc = ResourceLeakUtils.getResourceLeakChecker(this); rlc.setRoot(root); @@ -410,15 +430,12 @@ public boolean isResourceCollection(Tree tree) { * @return true if the method is annotated {@code @CreatesCollectionObligation} */ public boolean isCreatesCollectionObligationMethod(ExecutableElement methodElement) { - return getDeclAnnotation( - methodElement, - org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation.class) - != null; + return getDeclAnnotation(methodElement, CreatesCollectionObligation.class) != null; } /** * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} - * call, or null if the call has no such argument. + * call, or {@code null} if the call has no such argument. * *

The current heuristic is that the inserted argument is the last argument at the call site. * @@ -434,7 +451,7 @@ public boolean isCreatesCollectionObligationMethod(ExecutableElement methodEleme /** * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} - * call, or null if the call has no such argument. + * call, or {@code null} if the call has no such argument. * *

The current heuristic is that the inserted argument is the last argument at the call site. * @@ -472,17 +489,22 @@ public CollectionMutatorArgumentKind getCollectionMutatorArgumentKind(Tree inser RLCCalledMethodsAnnotatedTypeFactory rlAtf = ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); + // If the inserted element annotated as @NotOwning, then it's definitely not owning Element insertedElement = TreeUtils.elementFromTree(insertedArgumentTree); if (insertedElement != null && insertedElement.getAnnotation(NotOwning.class) != null) { return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; } + // If the inserted element is a parameter, and it has no explicit @Owning annotation, then it's + // definitely not owning. if (insertedElement != null && insertedElement.getKind() == ElementKind.PARAMETER) { return insertedElement.getAnnotation(Owning.class) == null ? CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING : CollectionMutatorArgumentKind.MAY_BE_OWNING; } + // If the inserted element is a method call, and the callee has no @Owning annotation, then it's + // definitely not owning. if (insertedArgumentTree instanceof MethodInvocationTree) { ExecutableElement callee = TreeUtils.elementFromUse((MethodInvocationTree) insertedArgumentTree); From 06e60fae7a05b394ed4be563a169210fb1a3075a Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Wed, 1 Apr 2026 14:57:11 -0700 Subject: [PATCH 349/374] Document lambda fixpoint lifecycle for resource leak analysis --- .../CollectionOwnershipAnnotatedTypeFactory.java | 4 ++-- .../RLCCalledMethodsAnnotatedTypeFactory.java | 12 ++++++++++-- .../framework/type/GenericAnnotatedTypeFactory.java | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 1bbcc9600d46..4a41c6f86882 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -255,7 +255,7 @@ public CollectionOwnershipStore getStoreForBlock( * *

This override exists because the Collection Ownership Checker currently runs last in the * Resource Leak Checker hierarchy. The last checker in that hierarchy is responsible for - * triggering the final resource-leak post-analysis for the method. + * triggering the method-level resource-leak post-analysis for the enclosing method. * * @param cfg the method CFG that has completed its first analysis */ @@ -270,7 +270,7 @@ protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { * *

If resource-leak-specific post-analysis already ran during {@link * #postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph)}, then this method avoids running it a - * second time. + * second time for the enclosing method. * * @param cfg the CFG to post-analyze */ diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index b257d450c7c0..43bf793f58c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -503,8 +503,16 @@ protected ControlFlowGraph analyze( boolean isStatic, @Nullable AccumulationStore capturedStore) { if (cfg != null && ast.getKind() == UnderlyingAST.Kind.METHOD) { - // Preserve the first method analysis result, but keep the framework's lambda loop running - // by re-enqueuing the nested classes and lambdas from the existing CFG. + // The old RLCC workaround for RLLambda.java (#7316) used to run in analyze(). It was moved + // to CollectionOwnershipAnnotatedTypeFactory.postAnalyzeAfterFirstMethodAnalysis(...) so it + // executes once at the correct lifecycle point: after the first method analysis and before + // lambda fixpoint. + // + // At this point cfg is the preserved first method analysis result. Keep that result, but + // re-enqueue nested classes and lambdas so later fixpoint iterations still analyze them. + // Returning cfg without re-enqueuing would incorrectly stop lambda processing; recomputing + // the method analysis would discard the preserved first-pass result that the early + // resource-leak post-analysis depends on. for (ClassTree cls : cfg.getDeclaredClasses()) { classQueue.add(IPair.of(cls, getStoreBefore(cls))); } 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 4d83606f4e3f..fac4c91f9f0a 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1524,6 +1524,10 @@ private void performFlowAnalysisForMethod( /* isStatic= */ false, capturedStore); if (firstIteration) { + // Some checkers need a method-level pass after the method CFG is analyzed once but before + // contained lambdas enter fixpoint. This hook was added so such logic does not have to live + // inside analyze(); the Resource Leak Checker's old workaround for RLLambda.java (#7316) + // was the motivating case. postAnalyzeAfterFirstMethodAnalysis(methodCFG); } boolean anyLambdaResultChanged = false; From 58219b094efdc9171f17bfff81ea792c26ef9aaf Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 2 Apr 2026 02:57:27 -0700 Subject: [PATCH 350/374] More documentation. --- .../CollectionOwnershipTransfer.java | 54 ++-- .../CollectionOwnershipVisitor.java | 56 ++-- .../collectionownership/messages.properties | 4 +- .../MustCallAnnotatedTypeFactory.java | 15 +- .../checker/mustcall/MustCallVisitor.java | 291 ++++++++++++------ .../MustCallConsistencyAnalyzer.java | 163 +++++++--- 6 files changed, 381 insertions(+), 202 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index e3de6cd273e8..855dc4d20f14 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -75,9 +75,12 @@ public TransferResult visitAssignment( CollectionOwnershipType rhsType = atypeFactory.getCoType(rhs, atypeFactory.getStoreBefore(node)); - // Ownership transfer from rhs into lhs usually. - // Special case: desugared assignments of a temporary array variable - // and rhs being owning resource collection field. + // Ownership usually transfers from the rhs into the lhs. + // Some assignments instead create a non-owning alias at the lhs: + // 1. desugared enhanced-for array temporaries + // 2. reads from owning collection fields + // 3. explicit @NotOwningCollection declarations + // In those cases the rhs keeps its existing ownership information. if (rhsType != null) { switch (rhsType) { case OwningCollection: @@ -88,20 +91,10 @@ public TransferResult visitAssignment( TreeUtils.elementFromTree(node.getExpression().getTree()))) { replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); } else { - CollectionOwnershipType declCoType = null; - if (node.getTree() instanceof VariableTree) { - VariableTree varTree = (VariableTree) node.getTree(); - VariableElement vtElement = TreeUtils.elementFromDeclaration(varTree); - if (vtElement != null) { - List vtType = vtElement.asType().getAnnotationMirrors(); - declCoType = atypeFactory.getCoType(vtType); - } - } - if (declCoType == CollectionOwnershipType.NotOwningCollection) { - replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); - } else { - replaceInStores(res, rhsJE, atypeFactory.NOTOWNINGCOLLECTION); - } + replaceInStores( + res, + hasExplicitNotOwningCollectionDeclaration(node) ? lhsJE : rhsJE, + atypeFactory.NOTOWNINGCOLLECTION); } break; default: @@ -117,8 +110,7 @@ public TransferResult visitAssignment( * elements of some collection. * * @param res the incoming transfer result - * @param tree the AST tree that is possibly the loop condition for a - * collection-obligation-fulfilling loop + * @param tree the AST tree that may represent a verified fulfilling-loop condition * @return the resulting transfer result */ private TransferResult @@ -147,6 +139,30 @@ public TransferResult visitAssignment( return res; } + /** + * Returns whether the given assignment is a variable declaration whose declared type is + * explicitly {@code @NotOwningCollection}. + * + *

Such a declaration initializes a non-owning alias at the lhs instead of consuming ownership + * from the rhs expression. + * + * @param node an assignment node + * @return true if the assignment is a variable declaration with declared type + * {@code @NotOwningCollection} + */ + private boolean hasExplicitNotOwningCollectionDeclaration(AssignmentNode node) { + if (!(node.getTree() instanceof VariableTree)) { + return false; + } + VariableTree variableTree = (VariableTree) node.getTree(); + VariableElement declaredElement = TreeUtils.elementFromDeclaration(variableTree); + if (declaredElement == null) { + return false; + } + List declaredType = declaredElement.asType().getAnnotationMirrors(); + return atypeFactory.getCoType(declaredType) == CollectionOwnershipType.NotOwningCollection; + } + @Override public TransferResult visitLessThan( LessThanNode node, TransferInput in) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index 972f6ae8e69b..c7649347db78 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -54,27 +54,27 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { - // Enforce additional policy for collection mutators. + // Enforce source-level mutator restrictions before base type-checking. enforceCreatesCollectionObligationPolicy(tree); return super.visitMethodInvocation(tree, p); } /** - * Enforces the "mutations on non-owning collections" policy for methods annotated - * {@code @CreatesCollectionObligation}. + * Enforces the mutator policy for methods annotated {@code @CreatesCollectionObligation}. * - *

Strategy: a {@code @NotOwningCollection} receiver may only accept an inserted argument that - * is definitely non-owning. Owning receivers are allowed; the resource-leak analysis models any - * collection obligation they create. + *

A {@code @NotOwningCollection} receiver may only accept an inserted argument that is + * definitely non-owning at the call site. Owning receivers are allowed; the resource-leak + * analysis models any collection obligation they create. * - *

Note: we intentionally do not add an index property to @CreatesCollectionObligation yet. We - * use a heuristic: the "inserted thing" is the last argument at the call site. TODO: Maybe later - * remove this heuristic and require and index on the CreatesCollectionObligation annotation to - * determine the inserted element's index. + *

This visitor only checks instance mutators on resource-collection receivers. The inserted + * argument is identified using the current {@code @CreatesCollectionObligation} heuristic in + * {@link CollectionOwnershipAnnotatedTypeFactory#getInsertedArgumentTree(MethodInvocationTree)}. + * + * @param tree the method invocation tree */ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) { - ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - if (!atypeFactory.isCreatesCollectionObligationMethod(methodElt)) { + ExecutableElement methodElement = TreeUtils.elementFromUse(tree); + if (!atypeFactory.isCreatesCollectionObligationMethod(methodElement)) { return; } ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree); @@ -86,40 +86,48 @@ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) return; } if (tree.getArguments().isEmpty()) { - // No "inserted thing" to validate. return; } CollectionOwnershipStore storeBefore = atypeFactory.getStoreBefore(tree); - CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType recvType = + CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType receiverType = getCoTypeAtTree(receiverTree, storeBefore); - if (recvType == null) { + if (receiverType == null) { return; } - ExpressionTree insertedTree = atypeFactory.getInsertedArgumentTree(tree); - if (insertedTree == null) { + ExpressionTree insertedArgumentTree = atypeFactory.getInsertedArgumentTree(tree); + if (insertedArgumentTree == null) { return; } CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind insertedArgumentKind = - atypeFactory.getCollectionMutatorArgumentKind(insertedTree); - String methodName = methodElt.getSimpleName().toString(); - switch (recvType) { + atypeFactory.getCollectionMutatorArgumentKind(insertedArgumentTree); + String methodName = methodElement.getSimpleName().toString(); + switch (receiverType) { case NotOwningCollection: if (insertedArgumentKind != CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind .DEFINITELY_NON_OWNING) { checker.reportError( - insertedTree, + insertedArgumentTree, "illegal.collection.mutator.owning.insert.into.notowning", methodName, - TreeUtils.toStringTruncated(insertedTree, 60)); + TreeUtils.toStringTruncated(insertedArgumentTree, 60)); } break; default: - // Owning receivers are handled by resource-leak analysis; bottom is ignored. + // Owning receivers are handled by CO analysis; bottom and other cases are intentionally + // ignored here. } } - /** Gets the flow-sensitive collection-ownership qualifier for {@code expr}, if available. */ + /** + * Returns the flow-sensitive collection-ownership qualifier for the given expression, if + * available. + * + * @param expr an expression tree + * @param storeBefore the store before {@code expr} + * @return the flow-sensitive collection-ownership qualifier for {@code expr}, or {@code null} if + * no such qualifier is available + */ private CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType getCoTypeAtTree( ExpressionTree expr, CollectionOwnershipStore storeBefore) { if (storeBefore != null) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties index 74de68f99cbd..bc903fec4304 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/messages.properties @@ -3,5 +3,5 @@ unfulfilled.field.obligations=The field %s may not have all of its @MustCall %s transfer.owningcollection.field.ownership=Method invocation unsafely transfers the ownership away from field %s. An @OwningCollection field can never lose ownership. illegal.type.annotation=Users may not write %s. static.resource.collection.field=The static field %s is unsoundly treated as non-static. -illegal.collection.mutator.owning.insert.into.notowning=Call to %s not allowed on a @NotOwningCollection receiver when inserted value may be owning: %s -collection.obligation.never.enforced=@MustCall method {0} may have never been invoked on the elements of {1} because control cannot reach a normal method exit after this point. +illegal.collection.mutator.owning.insert.into.notowning=Call to %s is not allowed on a @NotOwningCollection receiver when the inserted value may be owning: %s +collection.obligation.never.enforced=@MustCall method %s may never be invoked on the elements of %s because control cannot reach a normal method exit after this point. diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 2688639619f0..61f149b0b30e 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -162,9 +162,8 @@ public MustCallAnnotatedTypeFactory(BaseTypeChecker checker) { /** * Records a potentially fulfilling collection loop for the given enclosing method. * - *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method - * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method - * loop state used by RLCC. + *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + * this method does nothing. * * @param enclosingMethodTree the method containing the loop * @param collectionTree the collection iterated over by the loop @@ -199,9 +198,8 @@ public void recordPotentiallyFulfillingCollectionLoop( /** * Records a potentially fulfilling enhanced-for-loop for the given enclosing method. * - *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method - * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method - * loop state used by RLCC. + *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + * this method does nothing. * * @param enclosingMethodTree the method containing the loop * @param enhancedForLoopTree the enhanced-for-loop tree @@ -218,9 +216,8 @@ public void recordPotentiallyFulfillingEnhancedForLoop( /** * Records a CFG-resolved potentially fulfilling collection loop for the given enclosing method. * - *

If this Must Call factory is part of the Resource Leak Checker hierarchy, then this method - * forwards the loop to the RLCCalledMethodsAnnotatedTypeFactory, which retains the per-method - * loop state used by RLCC. + *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + * this method does nothing. * * @param enclosingMethodTree the method containing the loop * @param collectionTree the collection iterated over by the loop diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 34c5d3ee51c9..4b06f6eecbc0 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -4,7 +4,6 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.BlockTree; -import com.sun.source.tree.BreakTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.EnhancedForLoopTree; @@ -384,6 +383,14 @@ public Void visitWhileLoop(WhileLoopTree tree, Void p) { return super.visitWhileLoop(tree, p); } + /** + * Performs AST-only matching for enhanced-for-loops that may fulfill collection obligations. + * + *

The visitor records only the loop tree here. RLCC resolves the desugared iterator CFG shape + * later during post-analysis of the enclosing method. + * + * @param tree the enhanced-for-loop to inspect + */ @Override public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { detectPotentiallyFulfillingEnhancedForLoop(tree); @@ -403,21 +410,22 @@ private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree if (enclosingMethodTree == null) { return; } - ExpressionTree collectionTree = collectionTreeFromExpression(tree.getExpression()); if (collectionTree == null) { return; } - if (!ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(atypeFactory) .isResourceCollection(collectionTree)) { return; } - atypeFactory.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, tree); } - /** Condition-kind -> allowed extraction methods. */ + /** + * Description of an accepted while-loop header form. + * + *

Each header form determines which extraction methods are allowed in the loop body. + */ private static final class WhileSpec { final Set extractMethods; @@ -444,11 +452,16 @@ private static final class WhileSpec { "removeFirst", "removeLast", // Stack - "pop" - // If you want more later, add "getFirst"/"getLast" only if you also - // guard soundness (they don't remove elements) - ))); + "pop"))); + /** + * AST facts recovered from a matched while-loop header. + * + *

{@link #collectionTree} is the collection whose element obligations may be discharged. + * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link + * #collectionVarNameForBailout} names the collection variable whose writes should invalidate the + * match when present. + */ private static final class WhileHeaderMatch { final ExpressionTree collectionTree; // the owning collection expression to mark final @Nullable Name collectionVarNameForBailout; // for writes/bailouts @@ -467,46 +480,51 @@ private static final class WhileHeaderMatch { } } + /** + * Records a while-loop that may fulfill collection obligations. + * + *

This method performs AST matching plus the small amount of CFG lookup needed to identify the + * condition block, the conditional successor, the body entry block, and the extracted element + * node. RLCC resolves the remaining CFG-local fact, the loop update block, later during + * post-analysis. + * + *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and + * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, + * and {@code while (0 < q.size())}. + * + * @param tree the while-loop to inspect + */ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); if (enclosingMethodTree == null) { return; } - // 1) Match header ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); WhileHeaderMatch header = matchWhileHeader(condNoParens); if (header == null) { return; } - - // 2) Extract body statements - List bodyStmts; - if (tree.getStatement() instanceof BlockTree) { - bodyStmts = ((BlockTree) tree.getStatement()).getStatements(); - } else if (tree.getStatement() != null) { - bodyStmts = Collections.singletonList(tree.getStatement()); - } else { + // 2) Extract body statements. + List bodyStatements = getLoopBodyStatements(tree.getStatement()); + if (bodyStatements == null) { return; } - - // 3) Find exactly one extraction call in the body (soundness) + // 3) Find exactly one extraction call in the body to be sound. BodyExtraction extraction = findSingleExtractionInWhileBody( - bodyStmts, + bodyStatements, header.headerVar, header.collectionVarNameForBailout, header.spec.extractMethods); if (extraction == null) { return; } - - // Resolve CFG-local metadata *now* (except loopUpdateBlock). + // Resolve CFG-local metadata (except loopUpdateBlock). Block condBlock = firstBlockForTree(condNoParens); if (condBlock == null) { return; } - // Find the ConditionalBlock that branches on the while condition. ConditionalBlock cblock = findConditionalSuccessor(condBlock); if (cblock == null) { @@ -514,7 +532,6 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { Block peeled = peelExceptionBlocksToPred(condBlock); if (peeled != null) { cblock = findConditionalSuccessor(peeled); - condBlock = peeled; } } if (cblock == null) { @@ -531,15 +548,14 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { // 4) Record a potentially fulfilling collection loop. // - // We store: + // Store: // - collectionTree (resources / q / s) // - collectionElementTree (it.next() / q.poll() / s.pop()) // - condition tree (the while condition) - // atypeFactory.recordPotentiallyFulfillingCollectionLoop( enclosingMethodTree, header.collectionTree, - extraction.extractionCall, // IMPORTANT: element is the extraction call tree + extraction.extractionCall, condNoParens, loopBodyEntryBlock, cblock, @@ -563,8 +579,30 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return null; } - private @Nullable Block firstBlockForTree(Tree t) { - Set nodes = atypeFactory.getNodesForTree(t); + /** + * Returns the statements in a loop body, regardless of whether the body is a block. + * + * @param statement the loop body statement + * @return the loop body statements, or {@code null} if {@code statement} is {@code null} + */ + private @Nullable List getLoopBodyStatements( + @Nullable StatementTree statement) { + if (statement == null) { + return null; + } + return statement instanceof BlockTree + ? ((BlockTree) statement).getStatements() + : Collections.singletonList(statement); + } + + /** + * Returns the first CFG block associated with the given tree. + * + * @param tree a tree + * @return the first CFG block associated with {@code tree}, or {@code null} if none is known + */ + private @Nullable Block firstBlockForTree(Tree tree) { + Set nodes = atypeFactory.getNodesForTree(tree); if (nodes == null || nodes.isEmpty()) { return null; } @@ -577,22 +615,34 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return null; } - private @Nullable Node anyNodeForTree(Tree t) { - Set nodes = atypeFactory.getNodesForTree(t); + /** + * Returns an arbitrary CFG node associated with the given tree. + * + * @param tree a tree + * @return a CFG node associated with {@code tree}, or {@code null} if none is known + */ + private @Nullable Node anyNodeForTree(Tree tree) { + Set nodes = atypeFactory.getNodesForTree(tree); if (nodes == null || nodes.isEmpty()) { return null; } return nodes.iterator().next(); } - private @Nullable ConditionalBlock findConditionalSuccessor(Block b) { - for (Block succ : b.getSuccessors()) { + /** + * Returns the conditional successor reached from the given block, if one is immediately visible. + * + * @param block a CFG block + * @return the conditional successor of {@code block}, or {@code null} if none is found + */ + private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { + for (Block succ : block.getSuccessors()) { if (succ instanceof ConditionalBlock) { return (ConditionalBlock) succ; } } - if (b instanceof SingleSuccessorBlock) { - Block succ = ((SingleSuccessorBlock) b).getSuccessor(); + if (block instanceof SingleSuccessorBlock) { + Block succ = ((SingleSuccessorBlock) block).getSuccessor(); if (succ instanceof ConditionalBlock) { return (ConditionalBlock) succ; } @@ -600,8 +650,18 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return null; } - private @Nullable Block peelExceptionBlocksToPred(Block b) { - Block cur = b; + /** + * Walks backward through exception blocks to recover the predecessor block that leads to the + * actual loop conditional. + * + *

This is needed because loop conditions such as {@code iterator.hasNext()} may be represented + * by exception blocks before reaching the conditional branch. + * + * @param block a CFG block + * @return a predecessor block to retry from, or {@code null} if no such block is found + */ + private @Nullable Block peelExceptionBlocksToPred(Block block) { + Block cur = block; Set visitedBlocks = new HashSet<>(); while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { Set preds = cur.getPredecessors(); @@ -617,6 +677,15 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return cur; } + /** + * Matches supported while-loop header forms and returns the recovered loop facts. + * + *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code + * while (c.size() > 0)}, and {@code while (0 < c.size())}. + * + * @param cond the while-loop condition with parentheses removed + * @return the recovered header facts, or {@code null} if the header is unsupported + */ private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { // Case A: while (it.hasNext()) if (cond instanceof MethodInvocationTree) { @@ -656,6 +725,12 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return null; } + /** + * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. + * + * @param inner the expression under the logical complement + * @return the recovered header facts, or {@code null} if the expression does not match + */ private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { if (!(inner instanceof MethodInvocationTree)) { return null; @@ -668,33 +743,36 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { if (recv == null) { return null; } - Name varName = getNameFromExpressionTree(recv); if (varName == null) { return null; } - Element recvElt = TreeUtils.elementFromTree(recv); if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { return null; } - ExpressionTree colTree = collectionTreeFromExpression(recv); if (colTree == null) { return null; } - return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); } - private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree bt) { - Tree.Kind k = bt.getKind(); + /** + * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < + * c.size()}. + * + * @param condition the binary condition + * @return the recovered header facts, or {@code null} if the expression does not match + */ + private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { + Tree.Kind k = condition.getKind(); if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { return null; } - ExpressionTree left = TreeUtils.withoutParens(bt.getLeftOperand()); - ExpressionTree right = TreeUtils.withoutParens(bt.getRightOperand()); + ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); + ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); // Normalize: accept "c.size() > 0" or "0 < c.size()" MethodInvocationTree sizeCall = null; @@ -746,30 +824,51 @@ private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); } - private boolean isIsEmptyCall(MethodInvocationTree mit) { - ExpressionTree sel = mit.getMethodSelect(); + /** + * Returns whether the given invocation is an {@code isEmpty()} call with no arguments. + * + * @param invocation a method invocation + * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments + */ + private boolean isIsEmptyCall(MethodInvocationTree invocation) { + ExpressionTree sel = invocation.getMethodSelect(); if (!(sel instanceof MemberSelectTree)) { return false; } MemberSelectTree ms = (MemberSelectTree) sel; - return ms.getIdentifier().contentEquals("isEmpty") && mit.getArguments().isEmpty(); + return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); } - private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree mit) { - ExpressionTree sel = mit.getMethodSelect(); + /** + * Returns the explicit receiver of the given invocation, if present. + * + * @param invocation a method invocation + * @return the explicit receiver, or {@code null} if none exists + */ + private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { + ExpressionTree sel = invocation.getMethodSelect(); if (sel instanceof MemberSelectTree) { return ((MemberSelectTree) sel).getExpression(); } return null; } - /** Recover "col" from: Iterator it = col.iterator(); while (it.hasNext()) { ... } */ - private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver(ExpressionTree itExpr) { - if (itExpr == null) { + /** + * Recovers the collection expression from an iterator receiver in a header such as {@code while + * (it.hasNext())}. + * + *

This only recognizes local iterator variables initialized by {@code col.iterator()}. + * + * @param iteratorExpr the iterator receiver expression + * @return the collection expression, or {@code null} if it cannot be recovered + */ + private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( + ExpressionTree iteratorExpr) { + if (iteratorExpr == null) { return null; } - Element itElt = TreeUtils.elementFromTree(itExpr); + Element itElt = TreeUtils.elementFromTree(iteratorExpr); if (!(itElt instanceof VariableElement)) { return null; } @@ -809,6 +908,12 @@ private boolean isIsEmptyCall(MethodInvocationTree mit) { return collectionTreeFromExpression(colExpr); } + /** + * One extracted element use recovered from a while-loop body. + * + *

The extraction call is the expression that removes or advances to the next element, such as + * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. + */ private static final class BodyExtraction { final MethodInvocationTree extractionCall; // it.next()/q.poll()/s.pop() @@ -818,10 +923,18 @@ private static final class BodyExtraction { } /** - * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns null - * (conservative/sound). + * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code + * null}. + * + *

This matcher rejects writes to the iterator/header variable and, when present, to the + * collection variable itself, because such writes invalidate the header/body correspondence used + * by later CFG verification. * - *

Also rejects break/return and writes to iterator/collection vars. + * @param statements the loop body statements + * @param headerVar the iterator or collection variable constrained by the header + * @param collectionVarName the collection variable to protect from writes, if any + * @param allowedExtractMethods the extraction methods allowed by the matched header + * @return the unique extraction in the loop body, or {@code null} if the body is unsupported */ private @Nullable BodyExtraction findSingleExtractionInWhileBody( List statements, @@ -863,18 +976,6 @@ private void recordExtractionIfAny(ExpressionTree expr) { extraction[0] = mit; } - @Override - public Void visitBreak(BreakTree node, Void p) { - illegal.set(true); - return super.visitBreak(node, p); - } - - @Override - public Void visitReturn(ReturnTree node, Void p) { - illegal.set(true); - return super.visitReturn(node, p); - } - @Override public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { markWriteIfTargetsHeaderOrCollection(node.getVariable()); @@ -920,9 +1021,18 @@ public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { return new BodyExtraction(extraction[0]); } + /** + * Returns whether the given invocation is an allowed extraction call on the matched header + * variable. + * + * @param invocation a method invocation + * @param headerVar the iterator or collection variable constrained by the header + * @param allowedExtractMethods extraction methods permitted by the matched header form + * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} + */ private boolean isExtractionCallOnHeaderVar( - MethodInvocationTree mit, Name headerVar, Set allowedExtractMethods) { - ExpressionTree sel = mit.getMethodSelect(); + MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { + ExpressionTree sel = invocation.getMethodSelect(); if (!(sel instanceof MemberSelectTree)) { return false; } @@ -931,7 +1041,7 @@ private boolean isExtractionCallOnHeaderVar( if (!allowedExtractMethods.contains(methodName)) { return false; } - if (!mit.getArguments().isEmpty()) { + if (!invocation.getArguments().isEmpty()) { return false; } Name recv = getNameFromExpressionTree(ms.getExpression()); @@ -949,12 +1059,9 @@ private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { return; } - List loopBodyStatementList; - if (tree.getStatement() instanceof BlockTree) { - BlockTree blockT = (BlockTree) tree.getStatement(); - loopBodyStatementList = blockT.getStatements(); - } else { - loopBodyStatementList = Collections.singletonList(tree.getStatement()); + List loopBodyStatements = getLoopBodyStatements(tree.getStatement()); + if (loopBodyStatements == null) { + return; } StatementTree init = tree.getInitializer().get(0); ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); @@ -969,7 +1076,7 @@ private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { return; } ExpressionTree collectionElementTree = - getLastElementAccessIfLoopValid(loopBodyStatementList, identifierInHeader, iterator); + getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); if (collectionElementTree != null) { // Pattern match succeeded, now mark the loop in the respective datastructures. @@ -1089,18 +1196,16 @@ protected Name nameOfCollectionThatAllElementsAreCalledOn( /** * Check that the loop does not contain any writes to the loop iterator variable or to the - * collection variable itself, or any return/break statements. Extract the collection access tree - * ({@code arr[i]} or {@code collection.get(i)} where {@code i} is the iterator variable and - * {@code collection/arr} is consistent with the loop header) and return the last encountered such - * tree. + * collection variable itself. Extract the collection access tree ({@code arr[i]} or {@code + * collection.get(i)} where {@code i} is the iterator variable and {@code collection/arr} is + * consistent with the loop header) and return the last encountered such tree. * * @param statements list of statements of the loop body * @param identifierInHeader collection name if loop condition is {@code i < collection.size()} or * {@code i < arr.length} and {@code n} if loop condition is {@code i < n} * @param iterator the name of the loop iterator variable - * @return null if any writes to loop iterator variable or return/break statements are in {@code - * block}. Else, return the last encountered collection access tree consistent with the loop - * heaer if it exists and else null. + * @return {@code null} if the loop body writes to the iterator or collection variable; otherwise + * the last collection element access tree consistent with the loop header, if one exists */ private @Nullable ExpressionTree getLastElementAccessIfLoopValid( List statements, Name identifierInHeader, Name iterator) { @@ -1144,18 +1249,6 @@ public Void visitAssignment(AssignmentTree tree, Void p) { return super.visitAssignment(tree, p); } - @Override - public Void visitBreak(BreakTree bt, Void p) { - blockIsIllegal.set(true); - return super.visitBreak(bt, p); - } - - @Override - public Void visitReturn(ReturnTree rt, Void p) { - blockIsIllegal.set(true); - return super.visitReturn(rt, p); - } - // check whether corresponds to collection.get(i) @Override public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 2c4de600af6c..b83ff8f7e0ea 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -219,15 +219,31 @@ public enum MethodExitKind { ImmutableSet.copyOf(EnumSet.allOf(MethodExitKind.class)); } - /** CFG currently being analyzed by analyze(cfg). */ + /** + * CFG currently being analyzed by {@link #analyze(ControlFlowGraph)}. + * + *

This field is initialized at the start of {@code analyze} and is only used by helpers that + * reason about whether newly-created obligations can still reach the regular exit. + */ private @Nullable ControlFlowGraph currentCfg = null; /** - * Cached set of blocks that can reach a regular exit block, respecting - * getSuccessorsExceptIgnoredExceptions filtering. Computed lazily per CFG, only if needed. + * Cached set of blocks that can reach a regular exit block, respecting {@link + * #getSuccessorsExceptIgnoredExceptions(Block)} filtering. + * + *

This cache is scoped to {@link #currentCfg}. It is cleared at the start of each top-level + * {@link #analyze(ControlFlowGraph)} call and computed lazily only if collection-obligation + * reporting needs it. */ private @Nullable Set blocksThatCanReachRegularExit = null; + /** + * Trees for which collection.obligation.never.enforced has already been reported in the current + * CFG analysis. + * + *

This suppresses duplicate diagnostics when the same invocation is visited on multiple + * obligation states during the worklist traversal. + */ private final Set reportedNeverEnforcedSites = new HashSet<>(); /** @@ -736,8 +752,10 @@ public MustCallConsistencyAnalyzer(ResourceLeakChecker rlc, boolean isLoopBodyAn } /** - * Ensures blocksThatCanReachRegularExit is computed for currentCfg. Safe to call multiple times; - * the computation runs at most once per CFG. + * Ensures {@link #blocksThatCanReachRegularExit} has been computed for {@link #currentCfg}. + * + *

This method is idempotent. The expensive reverse-reachability computation runs at most once + * per top-level CFG analysis. */ private void ensureBlocksThatCanReachRegularExitComputed() { if (blocksThatCanReachRegularExit != null) { @@ -750,20 +768,35 @@ private void ensureBlocksThatCanReachRegularExitComputed() { } /** - * Returns true iff {@code b} can reach the regular method exit along edges that are not filtered - * out by getSuccessorsExceptIgnoredExceptions. + * Returns whether {@code block} can reach the regular method exit in the current CFG. + * + *

Reachability is computed with the same filtered exceptional successors used by the main + * obligation traversal, so ignored exceptional edges do not make a block count as + * regular-exit-reachable. + * + * @param block the block to query + * @return true if {@code block} can reach the regular exit along allowed successor edges */ - private boolean canReachRegularExit(Block b) { + private boolean canReachRegularExit(Block block) { ensureBlocksThatCanReachRegularExitComputed(); - return blocksThatCanReachRegularExit != null && blocksThatCanReachRegularExit.contains(b); + return blocksThatCanReachRegularExit != null && blocksThatCanReachRegularExit.contains(block); } /** - * Computes the set of blocks that can reach the regular exit. + * Computes the blocks in {@code cfg} that can reach the regular exit. * - *

Implementation: 1) enumerate reachable blocks (from entry) to avoid weird unreachable junk - * 2) seed with the regular exit block, if reachable 3) reverse BFS using predecessors, but only - * if the pred->succ edge is one of getSuccessorsExceptIgnoredExceptions(pred) + *

The computation uses the same filtered exceptional edges as the main analysis: + * + *

    + *
  1. Enumerate blocks reachable from the entry block. + *
  2. Seed the worklist with the regular exit block, if that block is itself reachable. + *
  3. Run a reverse traversal through predecessor edges, but only when the predecessor-to- + * successor edge is one that {@link #getSuccessorsExceptIgnoredExceptions(Block)} would + * follow during forward propagation. + *
+ * + * @param cfg the CFG currently being analyzed + * @return the set of blocks that can reach the regular exit */ private Set computeBlocksThatCanReachRegularExit(ControlFlowGraph cfg) { Block entry = cfg.getEntryBlock(); @@ -796,12 +829,16 @@ private Set computeBlocksThatCanReachRegularExit(ControlFlowGraph cfg) { } /** - * Returns true iff {@code succ} appears among getSuccessorsExceptIgnoredExceptions(pred). This is - * the key that keeps backward reachability consistent with your forward traversal. + * Returns whether {@code successor} is a forward edge that the analysis would traverse from + * {@code predecessor}. + * + * @param predecessor the source block of the candidate edge + * @param successor the target block of the candidate edge + * @return true if {@code successor} is an allowed successor of {@code predecessor} */ - private boolean isAllowedSuccessor(Block pred, Block succ) { - for (IPair p : getSuccessorsExceptIgnoredExceptions(pred)) { - if (p.first == succ) { + private boolean isAllowedSuccessor(Block predecessor, Block successor) { + for (IPair p : getSuccessorsExceptIgnoredExceptions(predecessor)) { + if (p.first == successor) { return true; } } @@ -822,6 +859,10 @@ private boolean isAllowedSuccessor(Block pred, Block succ) { * is tracking non-owning aliases necessary, because by definition they cannot be used to fulfill * must-call obligations. * + *

This method also initializes per-CFG state used by helpers that diagnose collection + * obligations at their creation sites when those obligations cannot reach the regular method + * exit. + * * @param cfg the control flow graph of the method to check */ // TODO: This analysis is currently implemented directly using a worklist; in the future, it @@ -829,6 +870,7 @@ private boolean isAllowedSuccessor(Block pred, Block succ) { public void analyze(ControlFlowGraph cfg) { this.currentCfg = cfg; this.blocksThatCanReachRegularExit = null; + this.reportedNeverEnforcedSites.clear(); // The `visited` set contains everything that has been added to the worklist, even if it has // not yet been removed and analyzed. @@ -918,18 +960,8 @@ private void addObligationsForCreatesCollectionObligationAnno( CollectionObligation.fromTree(receiverNode.getTree(), mustCallMethod)); } if (!mustCallValues.isEmpty()) { - // If this call is in a region with no path to the regular method exit, the - // obligation will never be enforced. - if (!canReachRegularExit(node.getBlock())) { - // Deduplication check per call-site tree - if (reportedNeverEnforcedSites.add(node.getTree())) { - checker.reportError( - node.getTree(), - "collection.obligation.never.enforced", - mustCallValues.get(0), - receiverNode.getTree().toString()); - } - } + reportNeverEnforcedCollectionObligationIfNeeded( + node, mustCallValues.get(0), receiverNode.getTree()); } } if (receiverIsOwningField) { @@ -939,7 +971,7 @@ private void addObligationsForCreatesCollectionObligationAnno( checkEnclosingMethodIsCreatesMustCallFor(receiverNode, enclosingMethodTree); } } - // consume the inserted elements obligation + // Transfer the inserted element's obligation to the owning collection receiver. consumeInsertedArgumentObligationIfSingleElementInsert(obligations, node); break; default: @@ -948,13 +980,37 @@ private void addObligationsForCreatesCollectionObligationAnno( } /** - * Models consumption of the inserted element's obligation by the receiver collection for - * {@code @CreatesCollectionObligation} calls on owning receivers. + * Reports a collection obligation that is created in a region with no path to the regular method + * exit. + * + *

Such an obligation will never reach the normal enforcement point in {@link #analyze}, so it + * should be diagnosed at the creation site instead of being silently dropped from checking. + * + * @param node the invocation that creates the collection obligation + * @param mustCallMethod one required method of the collection element type, used in the message + * @param receiverTree the receiver whose collection obligation is being created + */ + private void reportNeverEnforcedCollectionObligationIfNeeded( + MethodInvocationNode node, String mustCallMethod, Tree receiverTree) { + if (!canReachRegularExit(node.getBlock()) && reportedNeverEnforcedSites.add(node.getTree())) { + checker.reportError( + node.getTree(), + "collection.obligation.never.enforced", + mustCallMethod, + receiverTree.toString()); + } + } + + /** + * Models transfer of the inserted element's obligation to an owning collection receiver. * *

The inserted argument is identified by {@link * CollectionOwnershipAnnotatedTypeFactory#getInsertedArgumentNode(MethodInvocationNode)}. We only * consume obligations for single-element inserts, not bulk operations such as {@code addAll} or * {@code putAll}. + * + * @param obligations the currently tracked obligations + * @param node the collection-mutating invocation */ private void consumeInsertedArgumentObligationIfSingleElementInsert( Set obligations, MethodInvocationNode node) { @@ -970,8 +1026,8 @@ private void consumeInsertedArgumentObligationIfSingleElementInsert( return; } - // Remove any tracked obligations for the inserted value from the caller context: - // responsibility is now represented by the collection obligation. + // Remove any tracked obligations for the inserted value from the caller context; responsibility + // is now represented by the collection obligation on the receiver. if (inserted instanceof LocalVariableNode) { removeObligationsContainingVar(obligations, (LocalVariableNode) inserted); return; @@ -2037,7 +2093,7 @@ private void checkReassignmentToOwningCollectionField( coAtf.getCoType(removeCastsAndGetTmpVarIfPresent(lhs), coStore); if (lhsCoType == null) { if (TreeUtils.isConstructor(enclosingMethodTree)) { - // If its in the constructor, it may be the first assignment to the field. + // If this is in a constructor, it may be the field's first assignment. // TODO: after PR #7050 is merged, use that logic here to determine if first assignment. // Treat as first assignment into this.field: ownership transfers into the object. // So the constructor should not be forced to discharge the parameter’s obligation. @@ -2089,7 +2145,6 @@ private void checkReassignmentToOwningCollectionField( "Field assignment might overwrite field's current value"); return; default: - return; } } } @@ -3424,9 +3479,6 @@ public static String collectionToString(Collection bwos) { } } - // Loop body analysis verifies collection loops that may call required methods on each iterated - // element, which can satisfy collection obligations. - /** * Analyze the loop body of a CFG-resolved potentially fulfilling collection loop, as determined * by a pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or by a @@ -3556,9 +3608,27 @@ public void analyzeResolvedPotentiallyFulfillingCollectionLoop( } } + /** + * Computes the loop blocks that are on some filtered path from the loop body entry to the loop + * update block. + * + *

The returned set is the intersection of: + * + *

    + *
  • blocks reachable from {@code entry} using {@link + * #getSuccessorsExceptIgnoredExceptions(Block)}, and + *
  • blocks that can reach {@code update} using that same filtered edge relation. + *
+ * + *

The update block itself is excluded because callers handle that edge separately. + * + * @param entry the first block in the loop body + * @param update the loop update block + * @return the loop blocks that stay within the verified loop region + */ private Set computeLoopRegion(Block entry, Block update) { // Forward reachability (using getSuccessorsExceptIgnoredExceptions), - // and build the reverse graph *for the same filtered edges*. + // and build the reverse graph for the same filtered edges. Set forward = new HashSet<>(); Map> allowedPreds = new HashMap<>(); @@ -3574,27 +3644,22 @@ private Set computeLoopRegion(Block entry, Block update) { if (s == null) { continue; } - // Record filtered predecessor relation for backward traversal. allowedPreds.computeIfAbsent(s, k -> new LinkedHashSet<>()).add(b); - - // We allow edges *to* update, but we do not expand beyond update. + // Allow edges to update, but we do not expand beyond update. if (s == update) { continue; } - if (forward.add(s)) { wl.addLast(s); } } } - // If update isn't even reachable from entry under the filtered edge relation, - // region is empty (nothing meaningful to certify). + // region is empty. if (!forward.contains(update) && !allowedPreds.containsKey(update)) { return Collections.emptySet(); } - // Backward reachability to update, but ONLY using the filtered reverse edges we built. Set forwardPlus = new HashSet<>(forward); forwardPlus.add(update); @@ -3698,7 +3763,7 @@ private Set analyzeTypeOfCollectionElement( } } if (!hasAnyLiveAlias) { - // all aliases are dead; called methods is bottom + // if any alias is not reachable, then./ called methods is bottom return null; } From 5c6eeefc12775f6a0b120a7c7c4772747b147c7f Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 2 Apr 2026 14:39:15 -0700 Subject: [PATCH 351/374] Use the new annotation hierarchy API in RLCC --- ...ollectionOwnershipAnnotatedTypeFactory.java | 18 +++++++++--------- .../mustcall/MustCallAnnotatedTypeFactory.java | 2 +- .../resourceleak/ResourceLeakUtils.java | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 4a41c6f86882..f7b90fd9289b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -760,12 +760,12 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { AnnotatedDeclaredType receiverType = t.getReceiverType(); AnnotationMirror receiverAnno = - receiverType == null ? null : receiverType.getEffectiveAnnotationInHierarchy(TOP); + receiverType == null ? null : receiverType.getAnnotationInHierarchy(TOP); boolean receiverHasExplicitAnno = receiverAnno != null && !AnnotationUtils.areSameByName(BOTTOM, receiverAnno); AnnotatedTypeMirror returnType = t.getReturnType(); - AnnotationMirror returnAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror returnAnno = returnType.getAnnotationInHierarchy(TOP); boolean returnHasExplicitAnno = returnAnno != null && !AnnotationUtils.areSameByName(BOTTOM, returnAnno); @@ -808,7 +808,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { List superParamTypes = annotatedSuperMethod.getParameterTypes(); for (int i = 0; i < superParamTypes.size(); i++) { - AnnotationMirror paramAnno = paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror paramAnno = paramTypes.get(i).getAnnotationInHierarchy(TOP); boolean paramHasExplicitAnno = paramAnno != null && !AnnotationUtils.areSameByName(BOTTOM, paramAnno); if (!paramHasExplicitAnno) { @@ -827,7 +827,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { } // end "if (overriddenMethods != null)" if (isResourceCollection(returnType.getUnderlyingType())) { - AnnotationMirror manualAnno = returnType.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror manualAnno = returnType.getAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { boolean isConstructor = t.getElement().getKind() == ElementKind.CONSTRUCTOR; if (isConstructor) { @@ -840,7 +840,7 @@ public Void visitExecutable(AnnotatedExecutableType t, Void p) { for (AnnotatedTypeMirror paramType : t.getParameterTypes()) { if (isResourceCollection(paramType.getUnderlyingType())) { - AnnotationMirror manualAnno = paramType.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror manualAnno = paramType.getAnnotationInHierarchy(TOP); if (manualAnno == null || AnnotationUtils.areSameByName(BOTTOM, manualAnno)) { paramType.replaceAnnotation(NOTOWNINGCOLLECTION); } @@ -863,7 +863,7 @@ protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, b if (elt != null) { boolean isField = elt.getKind() == ElementKind.FIELD; if (isField && isResourceCollection(type.getUnderlyingType())) { - AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror fieldAnno = type.getAnnotationInHierarchy(TOP); if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { TreePath currentPath = getPath(tree); MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); @@ -890,7 +890,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { if (elt instanceof VariableElement) { if (isResourceCollection(type.getUnderlyingType())) { if (elt.getKind() == ElementKind.FIELD) { - AnnotationMirror fieldAnno = type.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror fieldAnno = type.getAnnotationInHierarchy(TOP); if (fieldAnno == null || AnnotationUtils.areSameByName(BOTTOM, fieldAnno)) { type.replaceAnnotation(OWNINGCOLLECTION); } @@ -903,7 +903,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { List paramTypes = annotatedMethod.getParameterTypes(); for (int i = 0; i < params.size(); i++) { if (params.get(i).getSimpleName() == elt.getSimpleName()) { - type.replaceAnnotation(paramTypes.get(i).getEffectiveAnnotationInHierarchy(TOP)); + type.replaceAnnotation(paramTypes.get(i).getAnnotationInHierarchy(TOP)); break; } } @@ -913,7 +913,7 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { // as @NotOwningCollection at identifier use sites because of the defaulting path. For // plain non-collection locals, collection-ownership qualifiers are not meaningful, so // override that fallback here and keep them at bottom. - AnnotationMirror localAnno = type.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror localAnno = type.getAnnotationInHierarchy(TOP); if (localAnno == null || AnnotationUtils.areSameByName(TOP, localAnno)) { type.replaceAnnotation(BOTTOM); } diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 61f149b0b30e..0fecf2aaad76 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -303,7 +303,7 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar continue; } } - AnnotationMirror mcAnno = typeArg.getEffectiveAnnotationInHierarchy(TOP); + AnnotationMirror mcAnno = typeArg.getAnnotationInHierarchy(TOP); boolean typeArgIsMcUnknown = mcAnno != null && processingEnv diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 34b5c434d5e1..5369362b3abc 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -414,7 +414,7 @@ public static List getValuesInAnno(AnnotationMirror anno, ExecutableElem */ public static List getMcValues( AnnotatedTypeMirror type, MustCallAnnotatedTypeFactory mcAtf) { - AnnotationMirror anno = type.getEffectiveAnnotationInHierarchy(mcAtf.TOP); + AnnotationMirror anno = type.getAnnotationInHierarchy(mcAtf.TOP); if (anno == null) { return Collections.emptyList(); } From 91e9f76e7dd5509298391d68d6cad06aceef8d53 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Fri, 3 Apr 2026 02:15:56 -0700 Subject: [PATCH 352/374] Fix RLCC typecheck and misc CI issues --- ...llectionOwnershipAnnotatedTypeFactory.java | 5 ++- .../CollectionOwnershipTransfer.java | 2 +- .../checker/mustcall/MustCallVisitor.java | 45 +++++++++++++++---- .../MustCallConsistencyAnalyzer.java | 4 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 44 ++++++++++++++++-- 5 files changed, 85 insertions(+), 15 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index f7b90fd9289b..c6a23e6780de 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -614,7 +614,10 @@ public List getMustCallValuesOfResourceCollectionComponent(TypeMirror t) * @param coStore the store * @return the {@code CollectionOwnershipType} that the given node has in the given store */ - public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coStore) { + public CollectionOwnershipType getCoType(Node node, @Nullable CollectionOwnershipStore coStore) { + if (coStore == null) { + return null; + } JavaExpression jx = JavaExpression.fromNode(node); CFValue storeVal; try { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 855dc4d20f14..7ec9513784e0 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -249,7 +249,7 @@ private TransferResult transferOwnershipForMe atypeFactory.getCoType(arg, atypeFactory.getStoreBefore(node)); CollectionOwnershipType paramType = atypeFactory.getCoType(param.asType().getAnnotationMirrors()); - if (paramType == null) { + if (argType == null || paramType == null) { continue; } diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 4b06f6eecbc0..8d8f488439c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -427,18 +427,26 @@ private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree *

Each header form determines which extraction methods are allowed in the loop body. */ private static final class WhileSpec { + /** Methods that may extract an element when this header form is used. */ final Set extractMethods; + /** + * Creates a while-loop header specification. + * + * @param extractMethods methods that may extract an element from the looped collection + */ WhileSpec(Set extractMethods) { this.extractMethods = extractMethods; } } - // Iterator: while (it.hasNext()) { ... it.next() ... } + /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); - // Queue/Deque/Stack: while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... } - // Also size() > 0 / 0 < size() + /** + * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... + * }}, including {@code size() > 0} and {@code 0 < size()} variants. + */ private static final WhileSpec NONEMPTY_SPEC = new WhileSpec( new HashSet<>( @@ -463,11 +471,26 @@ private static final class WhileSpec { * match when present. */ private static final class WhileHeaderMatch { - final ExpressionTree collectionTree; // the owning collection expression to mark - final @Nullable Name collectionVarNameForBailout; // for writes/bailouts - final Name headerVar; // iterator var (it) OR collection var (q) + /** Owning collection expression whose element obligations may be discharged. */ + final ExpressionTree collectionTree; + + /** Collection variable name whose writes should invalidate the match, if one exists. */ + final @Nullable Name collectionVarNameForBailout; + + /** Iterator variable or collection variable constrained by the loop header. */ + final Name headerVar; + + /** Accepted extraction shape for the matched loop header. */ final WhileSpec spec; + /** + * Creates a summary of the AST facts recovered from a matched while-loop header. + * + * @param collectionTree the owning collection expression to mark + * @param collectionVarNameForBailout collection variable whose writes invalidate the match + * @param headerVar iterator or collection variable constrained by the header + * @param spec accepted extraction shape for the matched loop header + */ WhileHeaderMatch( ExpressionTree collectionTree, @Nullable Name collectionVarNameForBailout, @@ -915,8 +938,14 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ private static final class BodyExtraction { - final MethodInvocationTree extractionCall; // it.next()/q.poll()/s.pop() - + /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ + final MethodInvocationTree extractionCall; + + /** + * Creates a body extraction summary. + * + * @param extractionCall extraction call found in the loop body + */ BodyExtraction(MethodInvocationTree extractionCall) { this.extractionCall = extractionCall; } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 632c622565d3..b29880e015ce 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -838,7 +838,7 @@ private Set computeBlocksThatCanReachRegularExit(ControlFlowGraph cfg) { */ private boolean isAllowedSuccessor(Block predecessor, Block successor) { for (IPair p : getSuccessorsExceptIgnoredExceptions(predecessor)) { - if (p.first == successor) { + if (Objects.equals(p.first, successor)) { return true; } } @@ -3647,7 +3647,7 @@ private Set computeLoopRegion(Block entry, Block update) { // Record filtered predecessor relation for backward traversal. allowedPreds.computeIfAbsent(s, k -> new LinkedHashSet<>()).add(b); // Allow edges to update, but we do not expand beyond update. - if (s == update) { + if (s.equals(update)) { continue; } if (forward.add(s)) { diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 43bf793f58c1..881d44aa39bb 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -177,9 +177,18 @@ private static final class WhileLoopResolutionCache { /** A back edge in the CFG. */ private static final class BlockEdge { + /** Source block of the back edge. */ final Block sourceBlock; + + /** Target block of the back edge. */ final Block targetBlock; + /** + * Creates a CFG back edge description. + * + * @param sourceBlock source block of the back edge + * @param targetBlock target block of the back edge + */ BlockEdge(Block sourceBlock, Block targetBlock) { this.sourceBlock = sourceBlock; this.targetBlock = targetBlock; @@ -264,7 +273,7 @@ private static Map> computeDominators( Map> dominators = new HashMap<>(); for (Block block : reachableBlocks) { - if (block == entryBlock) { + if (block.equals(entryBlock)) { dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); } else { dominators.put(block, new HashSet<>(reachableBlocks)); // TOP @@ -275,7 +284,7 @@ private static Map> computeDominators( do { changed = false; for (Block block : reachableBlocks) { - if (block == entryBlock) { + if (block.equals(entryBlock)) { continue; } @@ -359,7 +368,7 @@ private static Set naturalLoop( if (predecessor == null || !reachableBlocks.contains(predecessor)) { continue; } - if (loopBlocks.add(predecessor) && predecessor != targetBlock) { + if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { stack.push(predecessor); } } @@ -533,6 +542,17 @@ protected ControlFlowGraph analyze( capturedStore); } + /** + * Records a while-like collection loop that matched syntactically and still needs CFG resolution. + * + * @param enclosingMethodTree enclosing method that contains the loop + * @param collectionTree collection expression whose elements may be discharged + * @param collectionElementTree tree for the element extracted from the collection + * @param conditionTree loop condition tree + * @param loopBodyEntryBlock first block of the loop body + * @param loopConditionalBlock conditional block that controls the loop + * @param collectionElementNode CFG node for the extracted collection element + */ public void recordPotentiallyFulfillingCollectionLoop( MethodTree enclosingMethodTree, ExpressionTree collectionTree, @@ -553,6 +573,12 @@ public void recordPotentiallyFulfillingCollectionLoop( collectionElementNode)); } + /** + * Records an enhanced-for loop that matched syntactically and still needs CFG resolution. + * + * @param enclosingMethodTree enclosing method that contains the loop + * @param enhancedForLoopTree matched enhanced-for loop + */ public void recordPotentiallyFulfillingEnhancedForLoop( MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { getOrCreateMethodCollectionLoopState(enclosingMethodTree) @@ -560,6 +586,18 @@ public void recordPotentiallyFulfillingEnhancedForLoop( .add(enhancedForLoopTree); } + /** + * Records a collection loop whose CFG facts are fully resolved for the consistency analyzer. + * + * @param enclosingMethodTree enclosing method that contains the loop + * @param collectionTree collection expression whose elements may be discharged + * @param collectionElementTree tree for the element extracted from the collection + * @param conditionTree loop condition tree + * @param loopBodyEntryBlock first block of the loop body + * @param loopUpdateBlock block that updates the loop state before the next iteration + * @param loopConditionalBlock conditional block that controls the loop + * @param collectionElementNode CFG node for the extracted collection element + */ public void recordResolvedPotentiallyFulfillingCollectionLoop( MethodTree enclosingMethodTree, ExpressionTree collectionTree, From 93abd887e1741826708770ef9461a62d656b0b5b Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Fri, 3 Apr 2026 19:04:31 -0700 Subject: [PATCH 353/374] Type check issue. --- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 18ea4b261f70..85a635a963e1 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -108,11 +108,7 @@ private static boolean eq(Object x, Object y) { } @Override - public V add( - @OwningCollection LinkedHashKeyedSet this, - V o, - int conflictBehavior, - int equalBehavior) { + public V add(LinkedHashKeyedSet this, V o, int conflictBehavior, int equalBehavior) { K key = keyer.getKeyFor(o); V old = theMap.get(key); if (old == null From 2c2468a949f98b5099d79bc8c340b24b9c486ba9 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sat, 4 Apr 2026 00:17:36 -0700 Subject: [PATCH 354/374] Type check issue. --- .../afu/scenelib/util/coll/LinkedHashKeyedSet.java | 7 ++++++- .../RLCCalledMethodsAnnotatedTypeFactory.java | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 85a635a963e1..54b76650eb46 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.OwningCollection; import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; @@ -108,7 +109,11 @@ private static boolean eq(Object x, Object y) { } @Override - public V add(LinkedHashKeyedSet this, V o, int conflictBehavior, int equalBehavior) { + public V add( + @NotOwningCollection LinkedHashKeyedSet this, + V o, + int conflictBehavior, + int equalBehavior) { K key = keyer.getKeyFor(o); V old = theMap.get(key); if (old == null diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 881d44aa39bb..c5a4f5eeb397 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -137,6 +137,9 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated */ private static final class MethodCollectionLoopState { + /** Creates per-method loop state for syntactically matched collection-obligation loops. */ + private MethodCollectionLoopState() {} + /** Enhanced-for-loops that have been matched syntactically but still need CFG resolution. */ final Set potentiallyFulfillingEnhancedForLoops = new LinkedHashSet<>(); From 34c5027e278cbfbbf45e1bb9b3c7cbe9164a1c28 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sat, 4 Apr 2026 22:50:58 -0700 Subject: [PATCH 355/374] Made LinkedHashKeyedSet as NOC; the Map takes ownership of the resources. --- .../scenelib/util/coll/LinkedHashKeyedSet.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 54b76650eb46..219fcbc9fdc9 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -4,9 +4,6 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; -import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; -import org.checkerframework.checker.collectionownership.qual.OwningCollection; -import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; @@ -68,7 +65,7 @@ public void remove() { } @Override - public Iterator iterator(@PolyOwningCollection LinkedHashKeyedSet this) { + public Iterator iterator() { return new KeyedSetIterator(); } @@ -90,7 +87,7 @@ public T[] toArray(T[] a) { * @param old the element to be removed, if {@code behavior} is REPLACE * @return true if an element was removed */ - private boolean checkAdd(@OwningCollection LinkedHashKeyedSet this, int behavior, V old) { + private boolean checkAdd(int behavior, V old) { switch (behavior) { case REPLACE: remove(old); @@ -109,11 +106,7 @@ private static boolean eq(Object x, Object y) { } @Override - public V add( - @NotOwningCollection LinkedHashKeyedSet this, - V o, - int conflictBehavior, - int equalBehavior) { + public V add(V o, int conflictBehavior, int equalBehavior) { K key = keyer.getKeyFor(o); V old = theMap.get(key); if (old == null @@ -128,7 +121,7 @@ public boolean add(@Owning V o) { } @Override - public boolean remove(@OwningCollection LinkedHashKeyedSet this, Object o) { + public boolean remove(Object o) { return theValues.remove(o); } From d2d1a36446edf3eda634cf09dfbabddcff88fe09 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 5 Apr 2026 11:08:57 -0700 Subject: [PATCH 356/374] Type check issue. --- .../scenelib/util/coll/LinkedHashKeyedSet.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index 219fcbc9fdc9..eeb0c0f96071 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -4,6 +4,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; +import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; +import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; @@ -65,7 +67,7 @@ public void remove() { } @Override - public Iterator iterator() { + public Iterator iterator(@PolyOwningCollection LinkedHashKeyedSet this) { return new KeyedSetIterator(); } @@ -87,7 +89,8 @@ public T[] toArray(T[] a) { * @param old the element to be removed, if {@code behavior} is REPLACE * @return true if an element was removed */ - private boolean checkAdd(int behavior, V old) { + private boolean checkAdd( + @NotOwningCollection LinkedHashKeyedSet this, int behavior, V old) { switch (behavior) { case REPLACE: remove(old); @@ -106,7 +109,11 @@ private static boolean eq(Object x, Object y) { } @Override - public V add(V o, int conflictBehavior, int equalBehavior) { + public V add( + @NotOwningCollection LinkedHashKeyedSet this, + V o, + int conflictBehavior, + int equalBehavior) { K key = keyer.getKeyFor(o); V old = theMap.get(key); if (old == null @@ -121,7 +128,7 @@ public boolean add(@Owning V o) { } @Override - public boolean remove(Object o) { + public boolean remove(@NotOwningCollection LinkedHashKeyedSet this, Object o) { return theValues.remove(o); } From 9e18cc67ed32b45b4584bc66964cac63241fe114 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 9 Apr 2026 01:04:32 -0700 Subject: [PATCH 357/374] Handle zero-arg Arrays.asList() in must-call polymorphism --- .../checker/mustcall/MustCallAnnotatedTypeFactory.java | 5 +++-- .../resourceleak-collections/ArraysAsListZeroArgs.java | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 checker/tests/resourceleak-collections/ArraysAsListZeroArgs.java diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 0fecf2aaad76..78f63dacb162 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -493,8 +493,9 @@ protected void replace( AnnotatedTypeMirror type, AnnotationMirrorMap replacements) { AnnotationMirrorMap realReplacements = replacements; TypeElement typeElement = TypesUtils.getTypeElement(type.getUnderlyingType()); - // only customize replacement for type elements - if (typeElement != null) { + // Zero-arg polymorphic varargs calls such as Arrays.asList() can reach here with no + // computed replacement. The emptiness check avoids dereferencing replacements.get(POLY). + if (typeElement != null && !replacements.isEmpty()) { assert replacements.size() == 1 && replacements.containsKey(POLY); AnnotationMirror extantPolyAnnoReplacement = replacements.get(POLY); if (AnnotationUtils.areSameByName( diff --git a/checker/tests/resourceleak-collections/ArraysAsListZeroArgs.java b/checker/tests/resourceleak-collections/ArraysAsListZeroArgs.java new file mode 100644 index 000000000000..68731850cfaa --- /dev/null +++ b/checker/tests/resourceleak-collections/ArraysAsListZeroArgs.java @@ -0,0 +1,8 @@ +import java.util.Arrays; +import java.util.List; + +class ArraysAsListZeroArgs { + // Regression test for zero-arg varargs calls with @PolyMustCall. This used to crash the + // Resource Leak Checker while resolving the return type of Arrays.asList(). + static final List VALUES = Arrays.asList(); +} From 683040555f8af27ad282034ad11574390a351429 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 26 Apr 2026 15:27:00 -0700 Subject: [PATCH 358/374] Adds a hook in ATF, started moving code into CO. --- ...llectionOwnershipAnnotatedTypeFactory.java | 7 +- .../DisposalLoopCoordinator.java | 157 ++++++++++++++++++ .../type/GenericAnnotatedTypeFactory.java | 15 ++ 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index c6a23e6780de..9a93de6ac5f3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -151,14 +151,14 @@ public enum CollectionMutatorArgumentKind { /** * Maps the AST-tree corresponding to the loop condition of a collection-obligation-fulfilling - * loop to the loop wrapper. + * loop to the loop wrapper. TODO: remove */ private static final Map conditionToVerifiedFulfillingLoopMap = new HashMap<>(); /** * Maps the cfg-block corresponding to the loop conditional block of a - * collection-obligation-fulfilling loop to the loop wrapper. + * collection-obligation-fulfilling loop to the loop wrapper. TODO: remove */ private static final Map conditionalBlockToVerifiedFulfillingLoopMap = new HashMap<>(); @@ -181,6 +181,7 @@ public static void markFulfillingLoop( * * @param tree a tree that is potentially the condition for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given tree is the condition + * TODO: remove */ public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForCondition( Tree tree) { @@ -193,7 +194,7 @@ public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForCo * * @param block the block that is potentially the conditional block for a fulfilling loop * @return the collection-obligation-fulfilling loop for which the given block is the CFG - * conditional block + * conditional block TODO: remove */ public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForConditionalBlock( Block block) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java new file mode 100644 index 000000000000..d5be883de796 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java @@ -0,0 +1,157 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.Tree; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.node.Node; + +/** Coordinates CO-owned state for disposal loops and their MCCA proof results. */ +public final class DisposalLoopCoordinator { + + /** The Collection Ownership type factory that owns this coordinator. */ + @SuppressWarnings("unused") + private final CollectionOwnershipAnnotatedTypeFactory coAtf; + + /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ + private final Map conditionToDisposalLoopMap = new IdentityHashMap<>(); + + /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop} */ + private final Map conditionalBlockToDisposalLoopMap = + new IdentityHashMap<>(); + + /** Map from a {@link DisposalLoop} to the called-methods proven by MCCA for that loop. */ + private final IdentityHashMap> disposalLoopToProvenCalledMethodsMap = + new IdentityHashMap<>(); + + /** + * Creates a coordinator for disposal loops. + * + * @param coAtf the Collection Ownership type factory that owns this coordinator + */ + public DisposalLoopCoordinator(CollectionOwnershipAnnotatedTypeFactory coAtf) { + this.coAtf = Objects.requireNonNull(coAtf); + } + + /** + * Wrapper for a loop that iterates over a resource collection and may call methods on the + * iterated collection element. This class stores the loop metadata needed for CO transfer and + * MCCA proof. + */ + public static class DisposalLoop { + + /** The {@code ExpressionTree} for collection that this loop iterates over. */ + public final ExpressionTree expressionTree; + + /** The {@code Tree} for the iterated collection element by this loop. */ + public final Tree iteratedElementTree; + + /** The CFG {@code Node} for the iterated collection element by this loop. */ + public final Node iteratedElementNode; + + /** The condition {@code Tree} for this loop. */ + public final Tree loopConditionTree; + + /** The conditional {@code Block} corresponding to the loop condition. */ + public final ConditionalBlock loopConditionalBlock; + + /** The entry {@code Block} for this loop's body. */ + public final Block loopBodyEntryBlock; + + /** The loop-update {@code Block}. */ + public final Block loopUpdateBlock; + + /** + * Constructs a new {@code DisposalLoop}. + * + * @param collectionExpressionTree the {@code ExpressionTree} for the collection that this loop + * iterates over + * @param iteratedElementTree the {@code Tree} for the iterated collection element + * @param iteratedElementNode the CFG {@code Node} for the iterated collection element + * @param loopConditionTree the condition {@code Tree} for this loop + * @param loopConditionBlock the conditional {@code Block} corresponding to the loop condition + * @param loopBodyEntryBlock the entry {@code Block} for this loop's body + * @param loopUpdateBlock the loop-update {@code Block} + */ + public DisposalLoop( + ExpressionTree collectionExpressionTree, + Tree iteratedElementTree, + Node iteratedElementNode, + Tree loopConditionTree, + ConditionalBlock loopConditionBlock, + Block loopBodyEntryBlock, + Block loopUpdateBlock) { + this.expressionTree = Objects.requireNonNull(collectionExpressionTree); + this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); + this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); + this.loopConditionTree = Objects.requireNonNull(loopConditionTree); + this.loopConditionalBlock = Objects.requireNonNull(loopConditionBlock); + this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); + this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); + } + } + + /** + * Returns the {@link DisposalLoop} corresponding to the loop condition {@code tree}, if one + * exists. + * + * @param tree the condition tree + * @return the {@link DisposalLoop} for condition {@code tree} if exists, otherwise {@code null}. + */ + public @Nullable DisposalLoop getDisposalLoopForConditionTree(Tree tree) { + return conditionToDisposalLoopMap.get(tree); + } + + /** + * Returns the {@link DisposalLoop} corresponding to the loop conditional {@code block}, if one + * exists. + * + * @param block the loop-condition block + * @return the {@link DisposalLoop} for conditional {@code block} if exists, otherwise {@code + * null}. + */ + public @Nullable DisposalLoop getDisposalLoopForConditionBlock(Block block) { + return conditionalBlockToDisposalLoopMap.get(block); + } + + /** + * Returns the called-methods proven by MCCA for a disposal loop. + * + * @param disposalLoop the disposal loop + * @return the methods proven for {@code disposalLoop}, or {@code null} if none are populated. + */ + public @Nullable Set getProvenCalledMethods(DisposalLoop disposalLoop) { + return disposalLoopToProvenCalledMethodsMap.get(disposalLoop); + } + + /** + * Registers a disposal loop whose proof succeeded. + * + * @param disposalLoop the disposal loop + * @param provenCalledMethods the methods proven by MCCA for the disposal loop + */ + public void registerProvenDisposalLoop( + DisposalLoop disposalLoop, Set provenCalledMethods) { + Objects.requireNonNull(disposalLoop); + Objects.requireNonNull(provenCalledMethods); + + conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); + conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); + disposalLoopToProvenCalledMethodsMap.put( + disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(provenCalledMethods))); + } + + /** Clears all disposal loops and MCCA proof results in this coordinator. */ + public void clear() { + conditionToDisposalLoopMap.clear(); + conditionalBlockToDisposalLoopMap.clear(); + disposalLoopToProvenCalledMethodsMap.clear(); + } +} 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 9feeaf3ea2b3..23904c2c88a9 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1598,6 +1598,20 @@ private boolean containsAllVoidLambdas(Set lambdas) { */ protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) {} + /** + * Perform any additional operations after CFG construction, but before dataflow analysis runs on + * the CFG. + * + *

This hook is intended for checker-specific setup that needs the CFG before {@link + * CFAbstractAnalysis#performAnalysis(ControlFlowGraph, List)} is invoked from {@link + * #analyze(Queue, Queue, UnderlyingAST, List, ControlFlowGraph, boolean, boolean, boolean, + * CFAbstractStore)}. + * + * @param cfg the constructed CFG + * @param ast the underlying AST for the CFG + */ + protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) {} + /** Sorts a list of trees with the variables first. */ private final Comparator sortVariablesFirst = (t1, t2) -> { @@ -1647,6 +1661,7 @@ protected ControlFlowGraph analyze( reachableNodes.add(node.getTree()); } }); + postCFGConstruction(cfg, ast); } if (isInitializationCode) { Store initStore = !isStatic ? initializationStore : initializationStaticStore; From 3ca69c3cfb9a54070d1ba1c6c54b64bb4705d302 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 26 Apr 2026 15:32:21 -0700 Subject: [PATCH 359/374] Map -> IdentityHashMap. --- .../checker/collectionownership/DisposalLoopCoordinator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java index d5be883de796..c991abf32f6c 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java @@ -21,10 +21,10 @@ public final class DisposalLoopCoordinator { private final CollectionOwnershipAnnotatedTypeFactory coAtf; /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ - private final Map conditionToDisposalLoopMap = new IdentityHashMap<>(); + private final IdentityHashMap conditionToDisposalLoopMap = new IdentityHashMap<>(); /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop} */ - private final Map conditionalBlockToDisposalLoopMap = + private final IdentityHashMap conditionalBlockToDisposalLoopMap = new IdentityHashMap<>(); /** Map from a {@link DisposalLoop} to the called-methods proven by MCCA for that loop. */ From da31c9bd7ad12c9b3e8a42f5399d654bf57f077e Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 26 Apr 2026 17:08:48 -0700 Subject: [PATCH 360/374] edits in documentation. --- ...llectionOwnershipAnnotatedTypeFactory.java | 106 ++++++++++-------- .../CollectionOwnershipTransfer.java | 47 ++++---- .../DisposalLoopCoordinator.java | 35 +++--- .../MustCallConsistencyAnalyzer.java | 30 ++--- 4 files changed, 112 insertions(+), 106 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 9a93de6ac5f3..097e90622469 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -11,7 +11,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -83,6 +82,9 @@ public class CollectionOwnershipAnnotatedTypeFactory */ private final MustCallAnnotatedTypeFactory mcAtf; + /** Stores disposal loops, their metadata and MCCA called-method facts. */ + private final DisposalLoopCoordinator disposalLoopCoordinator; + /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -150,74 +152,80 @@ public enum CollectionMutatorArgumentKind { public static final String UNKNOWN_METHOD_NAME = "1UNKNOWN"; /** - * Maps the AST-tree corresponding to the loop condition of a collection-obligation-fulfilling - * loop to the loop wrapper. TODO: remove - */ - private static final Map - conditionToVerifiedFulfillingLoopMap = new HashMap<>(); - - /** - * Maps the cfg-block corresponding to the loop conditional block of a - * collection-obligation-fulfilling loop to the loop wrapper. TODO: remove + * Creates a CollectionOwnershipAnnotatedTypeFactory. + * + * @param checker the checker associated with this type factory */ - private static final Map - conditionalBlockToVerifiedFulfillingLoopMap = new HashMap<>(); + @SuppressWarnings("this-escape") + public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + NOTOWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, NotOwningCollection.class); + TOP = NOTOWNINGCOLLECTION; + OWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, OwningCollection.class); + OWNINGCOLLECTIONWITHOUTOBLIGATION = + AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); + BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); + POLY = AnnotationBuilder.fromClass(elements, PolyOwningCollection.class); + mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); + disposalLoopCoordinator = new DisposalLoopCoordinator(this); + this.postInit(); + } /** - * Marks the specified loop as fulfilling a collection obligation. + * Returns the {@link DisposalLoopCoordinator.DisposalLoop} for the given loop-condition tree, if + * one exists. * - * @param verifiedFulfillingLoop the verified loop wrapper + * @param tree the loop-condition tree + * @return the disposal loop for {@code tree}, or {@code null} if none exists */ - public static void markFulfillingLoop( - ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop) { - conditionToVerifiedFulfillingLoopMap.put( - verifiedFulfillingLoop.condition, verifiedFulfillingLoop); - conditionalBlockToVerifiedFulfillingLoopMap.put( - verifiedFulfillingLoop.loopConditionalBlock, verifiedFulfillingLoop); + public DisposalLoopCoordinator.DisposalLoop getDisposalLoopForConditionTree(Tree tree) { + return disposalLoopCoordinator.getDisposalLoopForConditionTree(tree); } /** - * Returns the collection-obligation-fulfilling loop for which the given tree is the condition. + * Returns the {@link DisposalLoopCoordinator.DisposalLoop} for the given loop-condition block, if + * one exists. * - * @param tree a tree that is potentially the condition for a fulfilling loop - * @return the collection-obligation-fulfilling loop for which the given tree is the condition - * TODO: remove + * @param block the loop-condition block + * @return the disposal loop for {@code block}, or {@code null} if none exists */ - public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForCondition( - Tree tree) { - return conditionToVerifiedFulfillingLoopMap.get(tree); + public DisposalLoopCoordinator.DisposalLoop getDisposalLoopForConditionBlock(Block block) { + return disposalLoopCoordinator.getDisposalLoopForConditionBlock(block); } /** - * Returns the collection-obligation-fulfilling loop for which the given block is the CFG - * conditional block. + * Returns the called-methods computed by MCCA for a disposal loop. * - * @param block the block that is potentially the conditional block for a fulfilling loop - * @return the collection-obligation-fulfilling loop for which the given block is the CFG - * conditional block TODO: remove + * @param disposalLoop the disposal loop + * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are + * registered */ - public static ResolvedPotentiallyFulfillingCollectionLoop getFulfillingLoopForConditionalBlock( - Block block) { - return conditionalBlockToVerifiedFulfillingLoopMap.get(block); + public @Nullable Set getMccaCalledMethods( + DisposalLoopCoordinator.DisposalLoop disposalLoop) { + return disposalLoopCoordinator.getMCCACalledMethods(disposalLoop); } /** - * Creates a CollectionOwnershipAnnotatedTypeFactory. + * Registers the MCCA called-methods for an RLCC-resolved disposal loop. * - * @param checker the checker associated with this type factory + *

This bridge exists only until disposal-loop discovery and proof are fully coordinated by CO. + * + * @param resolvedLoop the RLCC-resolved loop whose MCCA analysis produced called-methods + * TODO:SM:COMEBACK TO THIS */ - @SuppressWarnings("this-escape") - public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - NOTOWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, NotOwningCollection.class); - TOP = NOTOWNINGCOLLECTION; - OWNINGCOLLECTION = AnnotationBuilder.fromClass(elements, OwningCollection.class); - OWNINGCOLLECTIONWITHOUTOBLIGATION = - AnnotationBuilder.fromClass(elements, OwningCollectionWithoutObligation.class); - BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); - POLY = AnnotationBuilder.fromClass(elements, PolyOwningCollection.class); - mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); - this.postInit(); + public void registerCalledMethodsForDisposalLoop( + ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop) { + DisposalLoopCoordinator.DisposalLoop disposalLoop = + new DisposalLoopCoordinator.DisposalLoop( + resolvedLoop.collectionTree, + resolvedLoop.collectionElementTree, + resolvedLoop.collectionElementNode, + resolvedLoop.condition, + resolvedLoop.loopConditionalBlock, + resolvedLoop.loopBodyEntryBlock, + resolvedLoop.loopUpdateBlock); + disposalLoopCoordinator.registerMCCACalledMethods( + disposalLoop, resolvedLoop.getCalledMethods()); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 7ec9513784e0..ee7f57cbf157 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -1,18 +1,20 @@ package org.checkerframework.checker.collectionownership; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import java.util.List; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; +import org.checkerframework.checker.collectionownership.DisposalLoopCoordinator.DisposalLoop; import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; @@ -105,30 +107,28 @@ public TransferResult visitAssignment( /** * May refine the type of the collection to @OwningCollectionWithoutObligation in the else store - * of the incoming transfer result. Does so if the given AST tree is the condition for a - * collection-obligation-fulfilling loop that calls all methods in the MustCall type of the - * elements of some collection. + * of the incoming transfer result. Does so if the given AST tree is the condition for a disposal + * loop whose MCCA called-methods cover the MustCall type of the collection's elements. * * @param res the incoming transfer result - * @param tree the AST tree that may represent a verified fulfilling-loop condition + * @param tree the AST tree that may represent a disposal-loop condition * @return the resulting transfer result */ - private TransferResult - updateStoreForVerifiedFulfillingCollectionLoop( - TransferResult res, Tree tree) { - ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop = - CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForCondition(tree); - if (verifiedFulfillingLoop != null) { + private TransferResult updateStoreForDisposalLoop( + TransferResult res, Tree tree) { + DisposalLoop disposalLoop = atypeFactory.getDisposalLoopForConditionTree(tree); + if (disposalLoop != null) { CollectionOwnershipStore elseStore = res.getElseStore(); - JavaExpression collectionJE = JavaExpression.fromTree(verifiedFulfillingLoop.collectionTree); + ExpressionTree collectionExpression = disposalLoop.expressionTree; + JavaExpression collectionJE = JavaExpression.fromTree(collectionExpression); + Set disposalLoopCalledMethods = atypeFactory.getMccaCalledMethods(disposalLoop); - CollectionOwnershipType collectionCoType = - atypeFactory.getCoType(verifiedFulfillingLoop.collectionTree); + CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionExpression); if (collectionCoType == CollectionOwnershipType.OwningCollection) { - List mustCallValuesOfElements = - atypeFactory.getMustCallValuesOfResourceCollectionComponent( - verifiedFulfillingLoop.collectionTree); - if (verifiedFulfillingLoop.getCalledMethods().containsAll(mustCallValuesOfElements)) { + List requiredElementMethods = + atypeFactory.getMustCallValuesOfResourceCollectionComponent(collectionExpression); + if (disposalLoopCalledMethods != null + && disposalLoopCalledMethods.containsAll(requiredElementMethods)) { elseStore.clearValue(collectionJE); elseStore.insertValue(collectionJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); return new ConditionalTransferResult<>( @@ -151,10 +151,9 @@ public TransferResult visitAssignment( * {@code @NotOwningCollection} */ private boolean hasExplicitNotOwningCollectionDeclaration(AssignmentNode node) { - if (!(node.getTree() instanceof VariableTree)) { + if (!(node.getTree() instanceof VariableTree variableTree)) { return false; } - VariableTree variableTree = (VariableTree) node.getTree(); VariableElement declaredElement = TreeUtils.elementFromDeclaration(variableTree); if (declaredElement == null) { return false; @@ -167,7 +166,7 @@ private boolean hasExplicitNotOwningCollectionDeclaration(AssignmentNode node) { public TransferResult visitLessThan( LessThanNode node, TransferInput in) { TransferResult res = super.visitLessThan(node, in); - return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); + return updateStoreForDisposalLoop(res, node.getTree()); } @Override @@ -180,7 +179,7 @@ public TransferResult visitMethodInvocation( ExecutableElement method = node.getTarget().getMethod(); List args = node.getArguments(); res = transferOwnershipForMethodInvocation(method, node, args, res); - res = updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); + res = updateStoreForDisposalLoop(res, node.getTree()); // Check whether the method is annotated @CreatesCollectionObligation. ExecutableElement methodElement = TreeUtils.elementFromUse(node.getTree()); @@ -214,14 +213,14 @@ public TransferResult visitMethodInvocation( public TransferResult visitConditionalNot( ConditionalNotNode node, TransferInput in) { TransferResult res = super.visitConditionalNot(node, in); - return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); + return updateStoreForDisposalLoop(res, node.getTree()); } @Override public TransferResult visitGreaterThan( GreaterThanNode node, TransferInput in) { TransferResult res = super.visitGreaterThan(node, in); - return updateStoreForVerifiedFulfillingCollectionLoop(res, node.getTree()); + return updateStoreForDisposalLoop(res, node.getTree()); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java index c991abf32f6c..937e31daad46 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java @@ -5,7 +5,6 @@ import java.util.Collections; import java.util.IdentityHashMap; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Objects; import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; @@ -21,14 +20,15 @@ public final class DisposalLoopCoordinator { private final CollectionOwnershipAnnotatedTypeFactory coAtf; /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ - private final IdentityHashMap conditionToDisposalLoopMap = new IdentityHashMap<>(); + private final IdentityHashMap conditionToDisposalLoopMap = + new IdentityHashMap<>(); /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop} */ private final IdentityHashMap conditionalBlockToDisposalLoopMap = new IdentityHashMap<>(); - /** Map from a {@link DisposalLoop} to the called-methods proven by MCCA for that loop. */ - private final IdentityHashMap> disposalLoopToProvenCalledMethodsMap = + /** Map from a {@link DisposalLoop} to the called-methods computed by MCCA for that loop. */ + private final IdentityHashMap> disposalLoopToMCCACalledMethodsMap = new IdentityHashMap<>(); /** @@ -105,7 +105,7 @@ public DisposalLoop( * @param tree the condition tree * @return the {@link DisposalLoop} for condition {@code tree} if exists, otherwise {@code null}. */ - public @Nullable DisposalLoop getDisposalLoopForConditionTree(Tree tree) { + public DisposalLoop getDisposalLoopForConditionTree(Tree tree) { return conditionToDisposalLoopMap.get(tree); } @@ -117,41 +117,40 @@ public DisposalLoop( * @return the {@link DisposalLoop} for conditional {@code block} if exists, otherwise {@code * null}. */ - public @Nullable DisposalLoop getDisposalLoopForConditionBlock(Block block) { + public DisposalLoop getDisposalLoopForConditionBlock(Block block) { return conditionalBlockToDisposalLoopMap.get(block); } /** - * Returns the called-methods proven by MCCA for a disposal loop. + * Returns the called-methods computed by MCCA for a disposal loop. * * @param disposalLoop the disposal loop - * @return the methods proven for {@code disposalLoop}, or {@code null} if none are populated. + * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are populated */ - public @Nullable Set getProvenCalledMethods(DisposalLoop disposalLoop) { - return disposalLoopToProvenCalledMethodsMap.get(disposalLoop); + public @Nullable Set getMCCACalledMethods(DisposalLoop disposalLoop) { + return disposalLoopToMCCACalledMethodsMap.get(disposalLoop); } /** - * Registers a disposal loop whose proof succeeded. + * Registers a disposal loop together with the called-methods computed by MCCA for it. * * @param disposalLoop the disposal loop - * @param provenCalledMethods the methods proven by MCCA for the disposal loop + * @param MCCACalledMethods the called-methods computed by MCCA for the disposal loop */ - public void registerProvenDisposalLoop( - DisposalLoop disposalLoop, Set provenCalledMethods) { + public void registerMCCACalledMethods(DisposalLoop disposalLoop, Set MCCACalledMethods) { Objects.requireNonNull(disposalLoop); - Objects.requireNonNull(provenCalledMethods); + Objects.requireNonNull(MCCACalledMethods); conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); - disposalLoopToProvenCalledMethodsMap.put( - disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(provenCalledMethods))); + disposalLoopToMCCACalledMethodsMap.put( + disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(MCCACalledMethods))); } /** Clears all disposal loops and MCCA proof results in this coordinator. */ public void clear() { conditionToDisposalLoopMap.clear(); conditionalBlockToDisposalLoopMap.clear(); - disposalLoopToProvenCalledMethodsMap.clear(); + disposalLoopToMCCACalledMethodsMap.clear(); } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 776972fe640a..2aaf12e9d161 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -43,6 +43,7 @@ import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.collectionownership.CollectionOwnershipStore; +import org.checkerframework.checker.collectionownership.DisposalLoopCoordinator.DisposalLoop; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -2747,17 +2748,16 @@ private void propagateObligationsToSuccessorBlock( } } - // check whether this corresponds to the else-cfg-edge of a conditional block - // corresponding to the loop condition of a collection-obligation-fulfilling - // loop. If yes, don't propagate the collection obligations that are fulfilled - // inside the loop. - boolean isElseEdgeOfVerifiedFulfillingLoop = false; - ResolvedPotentiallyFulfillingCollectionLoop verifiedFulfillingLoop = - CollectionOwnershipAnnotatedTypeFactory.getFulfillingLoopForConditionalBlock(currentBlock); - if ((currentBlock instanceof ConditionalBlock) && verifiedFulfillingLoop != null) { - ConditionalBlock conditionalBlock = (ConditionalBlock) currentBlock; + // Check whether this corresponds to the else CFG edge of a conditional block for a disposal + // loop which fulfills the collections must-call obligation. If so, don't propagate the + // collection obligations discharged inside the loop. + boolean isElseEdgeOfDisposalLoop = false; + DisposalLoop disposalLoop = coAtf.getDisposalLoopForConditionBlock(currentBlock); + Set disposalLoopCalledMethods = null; + if ((currentBlock instanceof ConditionalBlock conditionalBlock) && disposalLoop != null) { if (conditionalBlock.getElseSuccessor().equals(successor)) { - isElseEdgeOfVerifiedFulfillingLoop = true; + isElseEdgeOfDisposalLoop = true; + disposalLoopCalledMethods = coAtf.getMccaCalledMethods(disposalLoop); } } @@ -2766,12 +2766,12 @@ private void propagateObligationsToSuccessorBlock( for (Obligation obligation : obligations) { - if (isElseEdgeOfVerifiedFulfillingLoop) { + if (isElseEdgeOfDisposalLoop && disposalLoopCalledMethods != null) { if (obligation instanceof CollectionObligation) { String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; - if (verifiedFulfillingLoop.getCalledMethods().contains(mustCallMethodOfCo)) { - // don't propagate this obligation along this edge, as it was fulfilled - // in the loop that the currentBlock is the conditional block of + if (disposalLoopCalledMethods.contains(mustCallMethodOfCo)) { + // Don't propagate this obligation along this edge, as the called-methods for this + // disposal loop already fulfills it. continue; } } @@ -3590,7 +3590,7 @@ public void analyzeResolvedPotentiallyFulfillingCollectionLoop( // Record the loop as verified if it calls any methods on the iterated element. if (calledMethodsInLoop != null && !calledMethodsInLoop.isEmpty()) { resolvedPotentiallyFulfillingLoop.addCalledMethods(calledMethodsInLoop); - CollectionOwnershipAnnotatedTypeFactory.markFulfillingLoop(resolvedPotentiallyFulfillingLoop); + coAtf.registerCalledMethodsForDisposalLoop(resolvedPotentiallyFulfillingLoop); } } From e27253ec6bfe56cedf67d93730fbb5ea6bf667cf Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Sun, 26 Apr 2026 17:30:23 -0700 Subject: [PATCH 361/374] Make MCCA only analyze and return the called-methods for a disposal loop. --- ...llectionOwnershipAnnotatedTypeFactory.java | 5 +- .../MustCallConsistencyAnalyzer.java | 59 ++++++++----------- .../RLCCalledMethodsAnnotatedTypeFactory.java | 8 ++- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 097e90622469..ec964b524d29 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -214,7 +214,7 @@ public DisposalLoopCoordinator.DisposalLoop getDisposalLoopForConditionBlock(Blo * TODO:SM:COMEBACK TO THIS */ public void registerCalledMethodsForDisposalLoop( - ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop) { + ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop, Set mccaCalledMethods) { DisposalLoopCoordinator.DisposalLoop disposalLoop = new DisposalLoopCoordinator.DisposalLoop( resolvedLoop.collectionTree, @@ -224,8 +224,7 @@ public void registerCalledMethodsForDisposalLoop( resolvedLoop.loopConditionalBlock, resolvedLoop.loopBodyEntryBlock, resolvedLoop.loopUpdateBlock); - disposalLoopCoordinator.registerMCCACalledMethods( - disposalLoop, resolvedLoop.getCalledMethods()); + disposalLoopCoordinator.registerMCCACalledMethods(disposalLoop, mccaCalledMethods); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 2aaf12e9d161..34fbc5010f9f 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -3466,54 +3466,44 @@ public static String collectionToString(Collection bwos) { } /** - * Analyze the loop body of a CFG-resolved potentially fulfilling collection loop, as determined - * by a pre-pattern-match in the MustCallVisitor (in the case of a normal for-loop) or by a - * pre-pattern-match in the RLCCalledMethodsAnnotatedTypeFactory (in the case of an - * enhanced-for-loop). - * - *

The analysis uses the CalledMethods type of the collection element iterated over to - * determine the methods the loop calls on the collection elements. + * Analyze the loop body of a {@link DisposalLoop} to compute the definitely called-methods on + * every iterated element on every path. * *

This method should be called after the called-method-analysis is finished (in the {@code * postAnalyze(cfg)} method of the {@code RLCCalledMethodsAnnotatedTypeFactory}). * * @param cfg the cfg of the enclosing method - * @param resolvedPotentiallyFulfillingLoop the loop to check + * @param disposalLoop the loop to analyze + * @return the called-methods for the loop on the iterated element, or {@code null} if there's no + * definite called-methods on the iterated element of the loop */ - public void analyzeResolvedPotentiallyFulfillingCollectionLoop( - ControlFlowGraph cfg, - ResolvedPotentiallyFulfillingCollectionLoop resolvedPotentiallyFulfillingLoop) { + public Set analyzeDisposalLoop( + ControlFlowGraph cfg, ResolvedPotentiallyFulfillingCollectionLoop disposalLoop) { // ensure checked loop is initialized in a valid way Objects.requireNonNull( - resolvedPotentiallyFulfillingLoop.collectionElementTree, + disposalLoop.collectionElementTree, "CollectionElementAccess tree provided to analyze loop body of an" + " CFG-resolved potentially fulfilling collection loop is null."); Objects.requireNonNull( - resolvedPotentiallyFulfillingLoop.loopBodyEntryBlock, + disposalLoop.loopBodyEntryBlock, "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + " loop is null."); Objects.requireNonNull( - resolvedPotentiallyFulfillingLoop.loopUpdateBlock, + disposalLoop.loopUpdateBlock, "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + " loop is null."); - Block loopBodyEntryBlock = resolvedPotentiallyFulfillingLoop.loopBodyEntryBlock; - Block loopUpdateBlock = resolvedPotentiallyFulfillingLoop.loopUpdateBlock; - Tree collectionElement = resolvedPotentiallyFulfillingLoop.collectionElementTree; + Block loopBodyEntryBlock = disposalLoop.loopBodyEntryBlock; + Block loopUpdateBlock = disposalLoop.loopUpdateBlock; + Tree collectionElement = disposalLoop.collectionElementTree; boolean emptyLoopBody = loopBodyEntryBlock.equals(loopUpdateBlock); if (emptyLoopBody) { - return; + return null; } - // The `visited` set contains everything that has been added to the worklist, even if it has - // not yet been removed and analyzed. - Set visited = new HashSet<>(); - Deque worklist = new ArrayDeque<>(); - // Add an obligation for the element of the collection iterated over - Obligation collectionElementObligation = Obligation.fromTree(collectionElement); if (collectionElement instanceof VariableTree) { VariableElement varElt = TreeUtils.elementFromDeclaration((VariableTree) collectionElement); @@ -3530,12 +3520,16 @@ public void analyzeResolvedPotentiallyFulfillingCollectionLoop( new BlockWithObligations( loopBodyEntryBlock, Collections.singleton(collectionElementObligation)); + // The `visited` set contains everything that has been added to the worklist, even if it has + // not yet been removed and analyzed. + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); worklist.add(loopBodyEntry); visited.add(loopBodyEntry); - Set calledMethodsInLoop = null; - Set loopRegion = computeLoopRegion(loopBodyEntryBlock, loopUpdateBlock); + Set calledMethodsInLoop = null; + // main loop: propagate obligations block-by-block while (!worklist.isEmpty()) { BlockWithObligations current = worklist.remove(); @@ -3554,12 +3548,12 @@ public void analyzeResolvedPotentiallyFulfillingCollectionLoop( boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock; boolean staysInLoop = loopRegion.contains(successorAndExceptionType.first); if (!isLastBlockOfBody && !staysInLoop) { - return; + return null; } if (isLastBlockOfBody) { Set calledMethodsAfterBlock = analyzeTypeOfCollectionElement( - currentBlock, resolvedPotentiallyFulfillingLoop, obligations, loopUpdateBlock); + currentBlock, disposalLoop, obligations, loopUpdateBlock); // intersect the called methods after this block with the accumulated ones so far. // This is required because there may be multiple "back edges" of the loop, in which // case we must intersect the called methods between those. @@ -3581,17 +3575,16 @@ public void analyzeResolvedPotentiallyFulfillingCollectionLoop( worklist); } catch (InvalidLoopBodyAnalysisException e) { // Expected when analyzing unreachable loop bodies. Safely abort the analysis. - return; + return null; } } } } - // Record the loop as verified if it calls any methods on the iterated element. - if (calledMethodsInLoop != null && !calledMethodsInLoop.isEmpty()) { - resolvedPotentiallyFulfillingLoop.addCalledMethods(calledMethodsInLoop); - coAtf.registerCalledMethodsForDisposalLoop(resolvedPotentiallyFulfillingLoop); + if (calledMethodsInLoop == null || calledMethodsInLoop.isEmpty()) { + return null; } + return new LinkedHashSet<>(calledMethodsInLoop); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 0b1a978d7cd2..7f448891ee46 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -1240,8 +1240,12 @@ private void analyzeResolvedPotentiallyFulfillingCollectionLoops( boolean loopContainedInThisMethod = cfg.getNodesCorrespondingToTree(collectionElementTree) != null; if (loopContainedInThisMethod) { - mustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop( - cfg, resolvedLoop); + Set disposalLoopCalledMethods = + mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, resolvedLoop); + if (disposalLoopCalledMethods != null) { + ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this) + .registerCalledMethodsForDisposalLoop(resolvedLoop, disposalLoopCalledMethods); + } resolvedLoopIterator.remove(); } } From af68bd506ebbd299e82446febe6cdb52dedbe8c2 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Mon, 27 Apr 2026 14:09:03 -0700 Subject: [PATCH 362/374] restructuring in place, need to document. --- ...llectionOwnershipAnnotatedTypeFactory.java | 224 +- .../CollectionOwnershipTransfer.java | 1 - .../CollectionOwnershipUtils.java | 254 ++ .../collectionownership/DisposalLoop.java | 62 + .../DisposalLoopCoordinator.java | 156 -- .../DisposalLoopScanner.java | 161 ++ .../EnhancedForDisposalLoopResolver.java | 210 ++ .../IndexedForDisposalLoopMatcher.java | 285 +++ .../WhileDisposalLoopMatcher.java | 870 +++++++ .../MustCallAnnotatedTypeFactory.java | 227 +- .../checker/mustcall/MustCallVisitor.java | 2144 ++++++++--------- .../MustCallConsistencyAnalyzer.java | 22 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 1841 +++++++------- .../RLCCalledMethodsTransfer.java | 24 +- 14 files changed, 4157 insertions(+), 2324 deletions(-) create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java delete mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index ec964b524d29..736507538c43 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -41,7 +41,6 @@ import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; @@ -82,8 +81,22 @@ public class CollectionOwnershipAnnotatedTypeFactory */ private final MustCallAnnotatedTypeFactory mcAtf; - /** Stores disposal loops, their metadata and MCCA called-method facts. */ - private final DisposalLoopCoordinator disposalLoopCoordinator; + // TODO: review moved disposal-loop docs in this file. + /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ + private final IdentityHashMap conditionToDisposalLoopMap = + new IdentityHashMap<>(); + + /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop}. */ + private final IdentityHashMap conditionalBlockToDisposalLoopMap = + new IdentityHashMap<>(); + + /** Map from a {@link DisposalLoop} to the called-methods computed by MCCA for that loop. */ + private final IdentityHashMap> disposalLoopToMCCACalledMethodsMap = + new IdentityHashMap<>(); + + /** Map from a method to the disposal loops discovered for it before CM analysis. */ + private final IdentityHashMap> + preparedDisposalLoopsByMethod = new IdentityHashMap<>(); /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -167,64 +180,203 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { BOTTOM = AnnotationBuilder.fromClass(elements, OwningCollectionBottom.class); POLY = AnnotationBuilder.fromClass(elements, PolyOwningCollection.class); mcAtf = ResourceLeakUtils.getMustCallAnnotatedTypeFactory(checker); - disposalLoopCoordinator = new DisposalLoopCoordinator(this); this.postInit(); } /** - * Returns the {@link DisposalLoopCoordinator.DisposalLoop} for the given loop-condition tree, if - * one exists. + * Returns the {@link DisposalLoop} corresponding to the loop condition {@code tree}, if one + * exists. * - * @param tree the loop-condition tree - * @return the disposal loop for {@code tree}, or {@code null} if none exists + * @param tree the condition tree + * @return the {@link DisposalLoop} for condition {@code tree} if exists, otherwise {@code null}. */ - public DisposalLoopCoordinator.DisposalLoop getDisposalLoopForConditionTree(Tree tree) { - return disposalLoopCoordinator.getDisposalLoopForConditionTree(tree); + public @Nullable DisposalLoop getDisposalLoopForConditionTree(Tree tree) { + return conditionToDisposalLoopMap.get(tree); } /** - * Returns the {@link DisposalLoopCoordinator.DisposalLoop} for the given loop-condition block, if - * one exists. + * Returns the {@link DisposalLoop} corresponding to the loop conditional {@code block}, if one + * exists. * * @param block the loop-condition block - * @return the disposal loop for {@code block}, or {@code null} if none exists + * @return the {@link DisposalLoop} for conditional {@code block} if exists, otherwise {@code + * null}. */ - public DisposalLoopCoordinator.DisposalLoop getDisposalLoopForConditionBlock(Block block) { - return disposalLoopCoordinator.getDisposalLoopForConditionBlock(block); + public @Nullable DisposalLoop getDisposalLoopForConditionBlock(Block block) { + return conditionalBlockToDisposalLoopMap.get(block); } /** * Returns the called-methods computed by MCCA for a disposal loop. * * @param disposalLoop the disposal loop - * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are - * registered + * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are populated + */ + public @Nullable Set getMccaCalledMethods(DisposalLoop disposalLoop) { + return disposalLoopToMCCACalledMethodsMap.get(disposalLoop); + } + + /** + * Returns true if the checker should ignore exceptional control flow due to the given exception + * type. + * + * @param exceptionType exception type + * @return true if {@code exceptionType} is ignored by collection-ownership flow */ - public @Nullable Set getMccaCalledMethods( - DisposalLoopCoordinator.DisposalLoop disposalLoop) { - return disposalLoopCoordinator.getMCCACalledMethods(disposalLoop); + @Override + public boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return analysis.isIgnoredExceptionType(exceptionType); } /** - * Registers the MCCA called-methods for an RLCC-resolved disposal loop. + * Registers a disposal loop together with the called-methods computed by MCCA for it. * - *

This bridge exists only until disposal-loop discovery and proof are fully coordinated by CO. + * @param disposalLoop the disposal loop + * @param MCCACalledMethods the called-methods computed by MCCA for the disposal loop + */ + private void registerMCCACalledMethods(DisposalLoop disposalLoop, Set MCCACalledMethods) { + conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); + conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); + disposalLoopToMCCACalledMethodsMap.put( + disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(MCCACalledMethods))); + } + + // /** + // * Registers the MCCA called-methods for an RLCC-resolved disposal loop. + // * + // *

This bridge exists only until disposal-loop discovery and proof are fully coordinated by + // CO. + // * + // * @param resolvedLoop the RLCC-resolved loop whose MCCA analysis produced called-methods + // */ + // public void registerCalledMethodsForDisposalLoop( + // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop, Set mccaCalledMethods) { + // DisposalLoop disposalLoop = + // new DisposalLoop( + // resolvedLoop.collectionTree, + // resolvedLoop.collectionElementTree, + // resolvedLoop.collectionElementNode, + // resolvedLoop.condition, + // resolvedLoop.loopConditionalBlock, + // resolvedLoop.loopBodyEntryBlock, + // resolvedLoop.loopUpdateBlock); + // registerMCCACalledMethods(disposalLoop, mccaCalledMethods); + // } + + /** + * Returns the disposal loops discovered in the given CFG. * - * @param resolvedLoop the RLCC-resolved loop whose MCCA analysis produced called-methods - * TODO:SM:COMEBACK TO THIS + * @param cfg the CFG to scan + * @return the disposal loops discovered in {@code cfg} */ - public void registerCalledMethodsForDisposalLoop( - ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop, Set mccaCalledMethods) { - DisposalLoopCoordinator.DisposalLoop disposalLoop = - new DisposalLoopCoordinator.DisposalLoop( - resolvedLoop.collectionTree, - resolvedLoop.collectionElementTree, - resolvedLoop.collectionElementNode, - resolvedLoop.condition, - resolvedLoop.loopConditionalBlock, - resolvedLoop.loopBodyEntryBlock, - resolvedLoop.loopUpdateBlock); - disposalLoopCoordinator.registerMCCACalledMethods(disposalLoop, mccaCalledMethods); + private Set discoverDisposalLoops(ControlFlowGraph cfg) { + if (cfg.getUnderlyingAST().getKind() != UnderlyingAST.Kind.METHOD) { + return Collections.emptySet(); + } + return new DisposalLoopScanner( + this, ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this), cfg) + .scanTree(((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod()); + } + + /** + * Discovers and stores the disposal loops for a method CFG so RLCC transfer can seed its initial + * store before called-methods analysis runs. + * + * @param cfg the method CFG whose disposal loops should be prepared + */ + public void prepareDisposalLoops(ControlFlowGraph cfg) { + MethodTree methodTree = getEnclosingMethodTree(cfg.getUnderlyingAST()); + if (methodTree == null) { + return; + } + preparedDisposalLoopsByMethod.put(methodTree, new LinkedHashSet<>(discoverDisposalLoops(cfg))); + } + + /** + * Returns the prepared disposal loops for the given underlying AST. + * + * @param underlyingAST the underlying AST whose prepared loops should be returned + * @return the prepared disposal loops for {@code underlyingAST} + */ + public Set getPreparedDisposalLoops(UnderlyingAST underlyingAST) { + MethodTree methodTree = getEnclosingMethodTree(underlyingAST); + if (methodTree == null) { + return Collections.emptySet(); + } + Set preparedDisposalLoops = preparedDisposalLoopsByMethod.get(methodTree); + if (preparedDisposalLoops == null) { + return Collections.emptySet(); + } + return Collections.unmodifiableSet(new LinkedHashSet<>(preparedDisposalLoops)); + } + + /** + * Removes and returns the prepared disposal loops for the given underlying AST. + * + * @param underlyingAST the underlying AST whose prepared loops should be removed + * @return the removed prepared disposal loops for {@code underlyingAST} + */ + private Set removePreparedDisposalLoops(UnderlyingAST underlyingAST) { + MethodTree methodTree = getEnclosingMethodTree(underlyingAST); + if (methodTree == null) { + return Collections.emptySet(); + } + Set preparedDisposalLoops = preparedDisposalLoopsByMethod.remove(methodTree); + if (preparedDisposalLoops == null) { + return Collections.emptySet(); + } + return preparedDisposalLoops; + } + + /** + * Returns the enclosing method tree for the given underlying AST, if it is a method CFG. + * + * @param underlyingAST the underlying AST + * @return the enclosing method tree, or {@code null} if {@code underlyingAST} is not a method + */ + private @Nullable MethodTree getEnclosingMethodTree(UnderlyingAST underlyingAST) { + if (underlyingAST.getKind() != UnderlyingAST.Kind.METHOD) { + return null; + } + return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); + } + + /** + * Certifies prepared disposal loops for a method CFG before collection-ownership analysis runs. + * + * @param cfg the method CFG + * @param ast the CFG's underlying AST + */ + @Override + protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { + certifyPreparedDisposalLoops(cfg, ast); + } + + /** + * Runs MCCA on the prepared disposal loops for a method CFG and registers any certified loops. + * + * @param cfg the method CFG + * @param ast the CFG's underlying AST + */ + private void certifyPreparedDisposalLoops(ControlFlowGraph cfg, UnderlyingAST ast) { + if (ast.getKind() != UnderlyingAST.Kind.METHOD) { + return; + } + + Set preparedDisposalLoops = removePreparedDisposalLoops(ast); + if (preparedDisposalLoops.isEmpty()) { + return; + } + + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = + new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); + for (DisposalLoop disposalLoop : preparedDisposalLoops) { + Set mccaCalledMethods = + mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoop); + if (mccaCalledMethods != null) { + registerMCCACalledMethods(disposalLoop, mccaCalledMethods); + } + } } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index ee7f57cbf157..7485685a0b26 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -10,7 +10,6 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; -import org.checkerframework.checker.collectionownership.DisposalLoopCoordinator.DisposalLoop; import org.checkerframework.checker.collectionownership.qual.CollectionFieldDestructor; import org.checkerframework.checker.collectionownership.qual.CreatesCollectionObligation; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java new file mode 100644 index 000000000000..fbd44b0f2a16 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java @@ -0,0 +1,254 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + +/** Utility methods shared by disposal-loop scanning and matcher resolution. */ +public final class CollectionOwnershipUtils { + + private CollectionOwnershipUtils() {} + + /** + * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a + * lambda expression or a different method. + * + * @param methodTree the method currently being scanned, or {@code null} + * @return the enclosing method for the current loop, or {@code null} if it is not part of the + * scanned method + */ + static @Nullable MethodTree getEnclosingMethodForCollectionLoop(@Nullable MethodTree methodTree) { + return methodTree; + } + + /** + * Returns the statements in a loop body, regardless of whether the body is a block. + * + * @param statement the loop body statement + * @return the loop body statements, or {@code null} if {@code statement} is {@code null} + */ + static @Nullable List getLoopBodyStatements( + @Nullable StatementTree statement) { + if (statement == null) { + return null; + } + return statement instanceof BlockTree + ? ((BlockTree) statement).getStatements() + : Collections.singletonList(statement); + } + + /** + * Returns the first CFG block associated with the given tree. + * + * @param cfg the CFG containing the tree + * @param tree a tree + * @return the first CFG block associated with {@code tree}, or {@code null} if none is known + */ + static @Nullable Block firstBlockForTree(ControlFlowGraph cfg, Tree tree) { + Set nodes = cfg.getNodesCorrespondingToTree(tree); + if (nodes == null || nodes.isEmpty()) { + return null; + } + for (Node n : nodes) { + Block block = n.getBlock(); + if (block != null) { + return block; + } + } + return null; + } + + /** + * Returns an arbitrary CFG node associated with the given tree. + * + * @param cfg the CFG containing the tree + * @param tree a tree + * @return a CFG node associated with {@code tree}, or {@code null} if none is known + */ + static @Nullable Node anyNodeForTree(ControlFlowGraph cfg, Tree tree) { + Set nodes = cfg.getNodesCorrespondingToTree(tree); + if (nodes == null || nodes.isEmpty()) { + return null; + } + return nodes.iterator().next(); + } + + /** + * Returns the tree to use as the loop-condition key. + * + * @param cfg the CFG containing the tree + * @param tree the original condition tree + * @return the CFG-associated tree for {@code tree}, if one exists; otherwise {@code tree} + */ + static Tree treeForLoopCondition(ControlFlowGraph cfg, Tree tree) { + Node node = anyNodeForTree(cfg, tree); + if (node != null && node.getTree() != null) { + return node.getTree(); + } + return tree; + } + + /** + * Returns the simple name of the identifier referenced by the given expression, or {@code null} + * if the expression does not reference an identifier. + * + * @param expr an expression + * @return the name of the referenced identifier, or {@code null} if none + */ + static Name getNameFromExpressionTree(ExpressionTree expr) { + if (expr == null) { + return null; + } + switch (expr.getKind()) { + case IDENTIFIER: + return ((com.sun.source.tree.IdentifierTree) expr).getName(); + case MEMBER_SELECT: + MemberSelectTree mst = (MemberSelectTree) expr; + Element elt = TreeUtils.elementFromUse(mst); + if (elt.getKind() == ElementKind.FIELD) { + return mst.getIdentifier(); + } else if (elt.getKind() == ElementKind.METHOD) { + return getNameFromExpressionTree(mst.getExpression()); + } else { + return null; + } + case METHOD_INVOCATION: + return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); + default: + return null; + } + } + + /** + * Returns the simple name of the identifier declared or referenced by the given statement, or + * {@code null} if the statement does not declare or reference an identifier. + * + * @param expr the {@code StatementTree} + * @return the name of the identifier declared or referenced by the statement, or {@code null} if + * none + */ + static Name getNameFromStatementTree(StatementTree expr) { + if (expr == null) { + return null; + } + switch (expr.getKind()) { + case VARIABLE: + return ((VariableTree) expr).getName(); + case EXPRESSION_STATEMENT: + return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); + default: + return null; + } + } + + /** + * Returns the ExpressionTree of the collection in the given expression. + * + * @param expr ExpressionTree + * @return the expression evaluates to or null if it doesn't + */ + static ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { + switch (expr.getKind()) { + case IDENTIFIER: + return expr; + case MEMBER_SELECT: + MemberSelectTree mst = (MemberSelectTree) expr; + Element elt = TreeUtils.elementFromUse(mst); + if (elt.getKind() == ElementKind.METHOD) { + return ((MemberSelectTree) expr).getExpression(); + } else if (elt.getKind() == ElementKind.FIELD) { + return expr; + } else { + return null; + } + case METHOD_INVOCATION: + return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); + default: + return null; + } + } + + /** + * Returns all successor blocks for some block, except for those corresponding to ignored + * exception types. + * + * @param block input block + * @param atypeFactory the CO type factory used to ignore exception types + * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for + * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor + */ + static Set> getSuccessorsExceptIgnoredExceptions( + Block block, CollectionOwnershipAnnotatedTypeFactory atypeFactory) { + if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { + ExceptionBlock exceptionBlock = (ExceptionBlock) block; + Set> result = new LinkedHashSet<>(); + Block regularSuccessor = exceptionBlock.getSuccessor(); + if (regularSuccessor != null) { + result.add(IPair.of(regularSuccessor, null)); + } + Map> exceptionalSuccessors = exceptionBlock.getExceptionalSuccessors(); + for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { + TypeMirror exceptionType = entry.getKey(); + if (!atypeFactory.isIgnoredExceptionType(exceptionType)) { + for (Block exceptionalSuccessor : entry.getValue()) { + result.add(IPair.of(exceptionalSuccessor, exceptionType)); + } + } + } + return result; + } else { + Set> result = new LinkedHashSet<>(); + for (Block successorBlock : block.getSuccessors()) { + result.add(IPair.of(successorBlock, null)); + } + return result; + } + } + + /** + * Returns blocks reachable from {@code entryBlock}. + * + * @param entryBlock the CFG entry block + * @return the reachable blocks + */ + public static Set reachableFrom(Block entryBlock) { + Set seen = new HashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(entryBlock); + seen.add(entryBlock); + + while (!queue.isEmpty()) { + Block block = queue.remove(); + for (Block successor : block.getSuccessors()) { + if (successor != null && seen.add(successor)) { + queue.add(successor); + } + } + } + return seen; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java new file mode 100644 index 000000000000..cda15afefe4e --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java @@ -0,0 +1,62 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.Tree; +import java.util.Objects; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.node.Node; + +/** Stores the resolved CFG and AST facts for one disposal loop. */ +public class DisposalLoop { + + /** The {@code ExpressionTree} for collection that this loop iterates over. */ + public final ExpressionTree expressionTree; + + /** The {@code Tree} for the iterated collection element by this loop. */ + public final Tree iteratedElementTree; + + /** The CFG {@code Node} for the iterated collection element by this loop. */ + public final Node iteratedElementNode; + + /** The condition {@code Tree} for this loop. */ + public final Tree loopConditionTree; + + /** The conditional {@code Block} corresponding to the loop condition. */ + public final ConditionalBlock loopConditionalBlock; + + /** The entry {@code Block} for this loop's body. */ + public final Block loopBodyEntryBlock; + + /** The loop-update {@code Block}. */ + public final Block loopUpdateBlock; + + /** + * Constructs a new {@code DisposalLoop}. + * + * @param collectionExpressionTree the {@code ExpressionTree} for the collection that this loop + * iterates over + * @param iteratedElementTree the {@code Tree} for the iterated collection element + * @param iteratedElementNode the CFG {@code Node} for the iterated collection element + * @param loopConditionTree the condition {@code Tree} for this loop + * @param loopConditionBlock the conditional {@code Block} corresponding to the loop condition + * @param loopBodyEntryBlock the entry {@code Block} for this loop's body + * @param loopUpdateBlock the loop-update {@code Block} + */ + public DisposalLoop( + ExpressionTree collectionExpressionTree, + Tree iteratedElementTree, + Node iteratedElementNode, + Tree loopConditionTree, + ConditionalBlock loopConditionBlock, + Block loopBodyEntryBlock, + Block loopUpdateBlock) { + this.expressionTree = Objects.requireNonNull(collectionExpressionTree); + this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); + this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); + this.loopConditionTree = Objects.requireNonNull(loopConditionTree); + this.loopConditionalBlock = Objects.requireNonNull(loopConditionBlock); + this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); + this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java deleted file mode 100644 index 937e31daad46..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopCoordinator.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.checkerframework.checker.collectionownership; - -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.Tree; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.node.Node; - -/** Coordinates CO-owned state for disposal loops and their MCCA proof results. */ -public final class DisposalLoopCoordinator { - - /** The Collection Ownership type factory that owns this coordinator. */ - @SuppressWarnings("unused") - private final CollectionOwnershipAnnotatedTypeFactory coAtf; - - /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ - private final IdentityHashMap conditionToDisposalLoopMap = - new IdentityHashMap<>(); - - /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop} */ - private final IdentityHashMap conditionalBlockToDisposalLoopMap = - new IdentityHashMap<>(); - - /** Map from a {@link DisposalLoop} to the called-methods computed by MCCA for that loop. */ - private final IdentityHashMap> disposalLoopToMCCACalledMethodsMap = - new IdentityHashMap<>(); - - /** - * Creates a coordinator for disposal loops. - * - * @param coAtf the Collection Ownership type factory that owns this coordinator - */ - public DisposalLoopCoordinator(CollectionOwnershipAnnotatedTypeFactory coAtf) { - this.coAtf = Objects.requireNonNull(coAtf); - } - - /** - * Wrapper for a loop that iterates over a resource collection and may call methods on the - * iterated collection element. This class stores the loop metadata needed for CO transfer and - * MCCA proof. - */ - public static class DisposalLoop { - - /** The {@code ExpressionTree} for collection that this loop iterates over. */ - public final ExpressionTree expressionTree; - - /** The {@code Tree} for the iterated collection element by this loop. */ - public final Tree iteratedElementTree; - - /** The CFG {@code Node} for the iterated collection element by this loop. */ - public final Node iteratedElementNode; - - /** The condition {@code Tree} for this loop. */ - public final Tree loopConditionTree; - - /** The conditional {@code Block} corresponding to the loop condition. */ - public final ConditionalBlock loopConditionalBlock; - - /** The entry {@code Block} for this loop's body. */ - public final Block loopBodyEntryBlock; - - /** The loop-update {@code Block}. */ - public final Block loopUpdateBlock; - - /** - * Constructs a new {@code DisposalLoop}. - * - * @param collectionExpressionTree the {@code ExpressionTree} for the collection that this loop - * iterates over - * @param iteratedElementTree the {@code Tree} for the iterated collection element - * @param iteratedElementNode the CFG {@code Node} for the iterated collection element - * @param loopConditionTree the condition {@code Tree} for this loop - * @param loopConditionBlock the conditional {@code Block} corresponding to the loop condition - * @param loopBodyEntryBlock the entry {@code Block} for this loop's body - * @param loopUpdateBlock the loop-update {@code Block} - */ - public DisposalLoop( - ExpressionTree collectionExpressionTree, - Tree iteratedElementTree, - Node iteratedElementNode, - Tree loopConditionTree, - ConditionalBlock loopConditionBlock, - Block loopBodyEntryBlock, - Block loopUpdateBlock) { - this.expressionTree = Objects.requireNonNull(collectionExpressionTree); - this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); - this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); - this.loopConditionTree = Objects.requireNonNull(loopConditionTree); - this.loopConditionalBlock = Objects.requireNonNull(loopConditionBlock); - this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); - this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); - } - } - - /** - * Returns the {@link DisposalLoop} corresponding to the loop condition {@code tree}, if one - * exists. - * - * @param tree the condition tree - * @return the {@link DisposalLoop} for condition {@code tree} if exists, otherwise {@code null}. - */ - public DisposalLoop getDisposalLoopForConditionTree(Tree tree) { - return conditionToDisposalLoopMap.get(tree); - } - - /** - * Returns the {@link DisposalLoop} corresponding to the loop conditional {@code block}, if one - * exists. - * - * @param block the loop-condition block - * @return the {@link DisposalLoop} for conditional {@code block} if exists, otherwise {@code - * null}. - */ - public DisposalLoop getDisposalLoopForConditionBlock(Block block) { - return conditionalBlockToDisposalLoopMap.get(block); - } - - /** - * Returns the called-methods computed by MCCA for a disposal loop. - * - * @param disposalLoop the disposal loop - * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are populated - */ - public @Nullable Set getMCCACalledMethods(DisposalLoop disposalLoop) { - return disposalLoopToMCCACalledMethodsMap.get(disposalLoop); - } - - /** - * Registers a disposal loop together with the called-methods computed by MCCA for it. - * - * @param disposalLoop the disposal loop - * @param MCCACalledMethods the called-methods computed by MCCA for the disposal loop - */ - public void registerMCCACalledMethods(DisposalLoop disposalLoop, Set MCCACalledMethods) { - Objects.requireNonNull(disposalLoop); - Objects.requireNonNull(MCCACalledMethods); - - conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); - conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); - disposalLoopToMCCACalledMethodsMap.put( - disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(MCCACalledMethods))); - } - - /** Clears all disposal loops and MCCA proof results in this coordinator. */ - public void clear() { - conditionToDisposalLoopMap.clear(); - conditionalBlockToDisposalLoopMap.clear(); - disposalLoopToMCCACalledMethodsMap.clear(); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java new file mode 100644 index 000000000000..33fe767f79c3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java @@ -0,0 +1,161 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.WhileLoopTree; +import com.sun.source.util.TreeScanner; +import java.util.LinkedHashSet; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; + +/** Scans one method tree and discovers disposal loops in its CFG. */ +public class DisposalLoopScanner extends TreeScanner { + + /** The CO type factory used for collection-ownership queries. */ + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + /** The RLCC type factory used for declaration lookup and ignored-exception checks. */ + private final RLCCalledMethodsAnnotatedTypeFactory rlccAtf; + + /** The CFG of the method currently being scanned. */ + private final ControlFlowGraph cfg; + + /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ + private final @Nullable MethodTree methodTree; + + /** Disposal loops discovered while scanning the current method tree. */ + private final Set disposalLoops = new LinkedHashSet<>(); + + /** Matcher for indexed `for` disposal loops. */ + private final IndexedForDisposalLoopMatcher indexedForDisposalLoopMatcher; + + /** Matcher for `while` disposal loops. */ + private final WhileDisposalLoopMatcher whileDisposalLoopMatcher; + + /** Resolver for enhanced-`for` disposal loops. */ + private final EnhancedForDisposalLoopResolver enhancedForDisposalLoopResolver; + + /** + * Creates a scanner for disposal loops in one method CFG. + * + * @param atypeFactory the CO type factory + * @param rlccAtf the RLCC type factory + * @param cfg the CFG to scan + */ + public DisposalLoopScanner( + CollectionOwnershipAnnotatedTypeFactory atypeFactory, + RLCCalledMethodsAnnotatedTypeFactory rlccAtf, + ControlFlowGraph cfg) { + this.atypeFactory = atypeFactory; + this.rlccAtf = rlccAtf; + this.cfg = cfg; + UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); + if (underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { + this.methodTree = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); + } else { + this.methodTree = null; + } + this.indexedForDisposalLoopMatcher = + new IndexedForDisposalLoopMatcher(this.atypeFactory, this.cfg, this.methodTree); + this.whileDisposalLoopMatcher = + new WhileDisposalLoopMatcher(this.atypeFactory, this.rlccAtf, this.cfg, this.methodTree); + this.enhancedForDisposalLoopResolver = + new EnhancedForDisposalLoopResolver(this.atypeFactory, this.cfg, this.methodTree); + } + + /** + * Scans a tree and returns the disposal loops discovered in it. + * + * @param tree the tree to scan + * @return the disposal loops discovered in {@code tree} + */ + public Set scanTree(Tree tree) { + disposalLoops.clear(); + scan(tree, null); + return new LinkedHashSet<>(disposalLoops); + } + + /** + * Skips nested lambdas when scanning the enclosing method. + * + * @param tree the lambda expression + * @param p the scan parameter + * @return always {@code null} + */ + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { + return null; + } + + /** + * Skips nested classes when scanning the enclosing method. + * + * @param tree the class tree + * @param p the scan parameter + * @return always {@code null} + */ + @Override + public Void visitClass(ClassTree tree, Void p) { + return null; + } + + /** + * Syntactically matches indexed for-loops that iterate over all elements of a collection. + * + * @param tree the for-loop to inspect + * @param p the scan parameter + * @return always {@code null} + */ + @Override + public Void visitForLoop(ForLoopTree tree, Void p) { + boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; + if (singleLoopVariable) { + DisposalLoop disposalLoop = indexedForDisposalLoopMatcher.match(tree); + if (disposalLoop != null) { + disposalLoops.add(disposalLoop); + } + } + return super.visitForLoop(tree, p); + } + + /** + * Matches while-loops that may fulfill collection obligations and resolves their CFG-local loop + * facts. + * + * @param tree the while-loop to inspect + * @param p the scan parameter + * @return always {@code null} + */ + @Override + public Void visitWhileLoop(WhileLoopTree tree, Void p) { + DisposalLoop disposalLoop = whileDisposalLoopMatcher.match(tree); + if (disposalLoop != null) { + disposalLoops.add(disposalLoop); + } + return super.visitWhileLoop(tree, p); + } + + /** + * Matches enhanced-for-loops that may fulfill collection obligations and resolves their desugared + * iterator CFG shape. + * + * @param tree the enhanced-for-loop to inspect + * @param p the scan parameter + * @return always {@code null} + */ + @Override + public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { + DisposalLoop disposalLoop = enhancedForDisposalLoopResolver.match(tree); + if (disposalLoop != null) { + disposalLoops.add(disposalLoop); + } + return super.visitEnhancedForLoop(tree, p); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java new file mode 100644 index 000000000000..2cb52e92b82d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -0,0 +1,210 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.VariableTree; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + +/** Resolves enhanced-`for` loops that may discharge collection obligations. */ +final class EnhancedForDisposalLoopResolver { + + /** The CO type factory used for collection-ownership queries. */ + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + /** The CFG of the method currently being scanned. */ + private final ControlFlowGraph cfg; + + /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ + private final @Nullable MethodTree methodTree; + + /** + * Creates a resolver for enhanced-`for` disposal loops. + * + * @param atypeFactory the CO type factory + * @param cfg the CFG being scanned + * @param methodTree the enclosing method, or {@code null} + */ + EnhancedForDisposalLoopResolver( + CollectionOwnershipAnnotatedTypeFactory atypeFactory, + ControlFlowGraph cfg, + @Nullable MethodTree methodTree) { + this.atypeFactory = atypeFactory; + this.cfg = cfg; + this.methodTree = methodTree; + } + + /** + * Adds an enhanced-for-loop that fulfills collection obligations. + * + * @param tree the enhanced-for-loop to inspect + * @return the matched disposal loop, or {@code null} if the loop does not match + */ + @Nullable DisposalLoop match(EnhancedForLoopTree tree) { + MethodTree enclosingMethodTree = + CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); + if (enclosingMethodTree == null) { + return null; + } + ExpressionTree collectionTree = + CollectionOwnershipUtils.collectionTreeFromExpression(tree.getExpression()); + if (collectionTree == null) { + return null; + } + if (!atypeFactory.isResourceCollection(collectionTree)) { + return null; + } + return resolveEnhancedForLoop(tree); + } + + /** + * Resolves an enhanced-for-loop candidate into a CFG-resolved loop. + * + * @param tree the enhanced-for-loop to resolve + * @return the CFG-resolved loop, or {@code null} if it cannot be resolved + */ + private @Nullable DisposalLoop resolveEnhancedForLoop(EnhancedForLoopTree tree) { + Block entryBlock = cfg.getEntryBlock(); + Set visitedBlocks = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + worklist.add(entryBlock); + visitedBlocks.add(entryBlock); + + while (!worklist.isEmpty()) { + Block currentBlock = worklist.removeFirst(); + + for (Node node : currentBlock.getNodes()) { + if (node instanceof MethodInvocationNode) { + DisposalLoop resolvedLoop = resolveEnhancedForLoop((MethodInvocationNode) node, tree); + if (resolvedLoop != null) { + return resolvedLoop; + } + } + } + + for (IPair successorAndExceptionType : + CollectionOwnershipUtils.getSuccessorsExceptIgnoredExceptions( + currentBlock, atypeFactory)) { + Block successorBlock = successorAndExceptionType.first; + if (successorBlock != null && visitedBlocks.add(successorBlock)) { + worklist.addLast(successorBlock); + } + } + } + + return null; + } + + /** + * Returns a resolved collection loop if the given node is desugared from an enhanced-for-loop + * over a collection. + * + * @param methodInvocationNode the node to check + * @param tree the enhanced-for-loop being resolved + * @return the resolved loop, or {@code null} if the node does not belong to {@code tree} + */ + private @Nullable DisposalLoop resolveEnhancedForLoop( + MethodInvocationNode methodInvocationNode, EnhancedForLoopTree tree) { + if (methodInvocationNode.getIterableExpression() == null) { + return null; + } + + EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); + if (loop == null) { + throw new BugInCF( + "MethodInvocationNode.iterableExpression should be non-null iff" + + " MethodInvocationNode.enhancedForLoop is non-null"); + } + if (loop != tree) { + return null; + } + + VariableTree loopVariable = loop.getVariable(); + + SingleSuccessorBlock singleSuccessorBlock = + (SingleSuccessorBlock) methodInvocationNode.getBlock(); + Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); + Node loopVariableNode = null; + Node node; + boolean isAssignmentOfLoopVariable; + do { + while (!nodeIterator.hasNext()) { + singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); + nodeIterator = singleSuccessorBlock.getNodes().iterator(); + } + node = nodeIterator.next(); + isAssignmentOfLoopVariable = false; + if ((node instanceof AssignmentNode) + && (node.getTree() instanceof VariableTree iteratorVariableDeclaration)) { + loopVariableNode = ((AssignmentNode) node).getTarget(); + isAssignmentOfLoopVariable = + iteratorVariableDeclaration.getName() == loopVariable.getName(); + } + } while (!isAssignmentOfLoopVariable); + Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); + + Block loopUpdateBlock = methodInvocationNode.getBlock(); + nodeIterator = loopUpdateBlock.getNodes().iterator(); + boolean isLoopCondition; + do { + while (!nodeIterator.hasNext()) { + Set predecessorBlocks = loopUpdateBlock.getPredecessors(); + if (predecessorBlocks.size() == 1) { + loopUpdateBlock = predecessorBlocks.iterator().next(); + nodeIterator = loopUpdateBlock.getNodes().iterator(); + } else { + return null; + } + } + node = nodeIterator.next(); + isLoopCondition = false; + if (node instanceof MethodInvocationNode) { + MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); + isLoopCondition = + methodInvocationTree != null && TreeUtils.isHasNextCall(methodInvocationTree); + } + } while (!isLoopCondition); + + Block blockContainingLoopCondition = node.getBlock(); + if (blockContainingLoopCondition.getSuccessors().size() != 1) { + throw new BugInCF( + "loop condition has: " + + blockContainingLoopCondition.getSuccessors().size() + + " successors instead of 1."); + } + Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + if (!(conditionalBlock instanceof ConditionalBlock)) { + throw new BugInCF( + "loop condition successor is not ConditionalBlock, but: " + conditionalBlock.getClass()); + } + if (loopVariableNode == null || loopVariableNode.getTree() == null || node.getTree() == null) { + return null; + } + + return new DisposalLoop( + loop.getExpression(), + loopVariableNode.getTree(), + loopVariableNode, + node.getTree(), + (ConditionalBlock) conditionalBlock, + loopBodyEntryBlock, + loopUpdateBlock); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java new file mode 100644 index 000000000000..1629cfc2ca51 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -0,0 +1,285 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.Name; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.TreeUtils; + +/** Matches indexed `for` loops that may discharge collection obligations. */ +final class IndexedForDisposalLoopMatcher { + + /** The CO type factory used for collection-ownership queries. */ + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + /** The CFG of the method currently being scanned. */ + private final ControlFlowGraph cfg; + + /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ + private final @Nullable MethodTree methodTree; + + /** + * Creates a matcher for indexed `for` disposal loops. + * + * @param atypeFactory the CO type factory + * @param cfg the CFG being scanned + * @param methodTree the enclosing method, or {@code null} + */ + IndexedForDisposalLoopMatcher( + CollectionOwnershipAnnotatedTypeFactory atypeFactory, + ControlFlowGraph cfg, + @Nullable MethodTree methodTree) { + this.atypeFactory = atypeFactory; + this.cfg = cfg; + this.methodTree = methodTree; + } + + /** + * Marks the for-loop if it potentially fulfills collection obligations of a collection. + * + * @param tree a `for` loop with exactly one loop variable + * @return the matched disposal loop, or {@code null} if the loop does not match + */ + @Nullable DisposalLoop match(ForLoopTree tree) { + MethodTree enclosingMethodTree = + CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); + if (enclosingMethodTree == null) { + return null; + } + + List loopBodyStatements = + CollectionOwnershipUtils.getLoopBodyStatements(tree.getStatement()); + if (loopBodyStatements == null) { + return null; + } + StatementTree init = tree.getInitializer().get(0); + ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); + ExpressionStatementTree update = tree.getUpdate().get(0); + if (!(condition instanceof BinaryTree)) { + return null; + } + Name identifierInHeader = + nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); + Name iterator = CollectionOwnershipUtils.getNameFromStatementTree(init); + if (identifierInHeader == null || iterator == null) { + return null; + } + ExpressionTree collectionElementTree = + getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); + if (collectionElementTree != null) { + Block loopConditionBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, condition); + Block loopUpdateBlock = + CollectionOwnershipUtils.firstBlockForTree(cfg, update.getExpression()); + Node nodeForCollectionElt = + CollectionOwnershipUtils.anyNodeForTree(cfg, collectionElementTree); + if (loopUpdateBlock == null || loopConditionBlock == null || nodeForCollectionElt == null) { + return null; + } + Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); + Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); + return new DisposalLoop( + CollectionOwnershipUtils.collectionTreeFromExpression(collectionElementTree), + collectionElementTree, + nodeForCollectionElt, + CollectionOwnershipUtils.treeForLoopCondition(cfg, condition), + (ConditionalBlock) conditionalBlock, + loopBodyEntryBlock, + loopUpdateBlock); + } + return null; + } + + /** + * Conservatively decides whether a loop iterates over all elements of some collection, using the + * following rules: + * + *

    + *
  • only one loop variable + *
  • initialization must be of the form i = 0 + *
  • condition must be of the form (i < col.size()) + *
  • update must be prefix or postfix {@code ++} + *
+ * + * Returns: + * + *
    + *
  • null, if any of the above rules is violated + *
  • the name of the collection if the loop condition is of the form (i < col.size()) + *
+ * + * @param init the initializer of the loop + * @param condition the loop condition + * @param update the loop update + * @return the name of the collection that the loop iterates over all elements of, or null + */ + Name nameOfCollectionThatAllElementsAreCalledOn( + StatementTree init, BinaryTree condition, ExpressionStatementTree update) { + Tree.Kind updateKind = update.getExpression().getKind(); + if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { + UnaryTree inc = (UnaryTree) update.getExpression(); + + if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) + return null; + VariableTree initVar = (VariableTree) init; + + if (!(initVar.getInitializer() instanceof LiteralTree) + || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { + return null; + } + + if (!(condition.getLeftOperand() instanceof IdentifierTree)) { + return null; + } + + Name initVarName = initVar.getName(); + if (initVarName != ((IdentifierTree) condition.getLeftOperand()).getName()) { + return null; + } + if (initVarName != ((IdentifierTree) inc.getExpression()).getName()) { + return null; + } + + if ((condition.getRightOperand() instanceof MethodInvocationTree) + && TreeUtils.isSizeAccess(condition.getRightOperand())) { + ExpressionTree methodSelect = + ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); + if (methodSelect instanceof MemberSelectTree) { + MemberSelectTree mst = (MemberSelectTree) methodSelect; + Element elt = TreeUtils.elementFromTree(mst.getExpression()); + if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { + return CollectionOwnershipUtils.getNameFromExpressionTree(mst.getExpression()); + } + } + } + } + return null; + } + + /** + * Check that the loop does not contain any writes to the loop iterator variable or to the + * collection variable itself. Extract the collection access tree ({@code arr[i]} or {@code + * collection.get(i)} where {@code i} is the iterator variable and {@code collection/arr} is + * consistent with the loop header) and return the last encountered such tree. + * + * @param statements list of statements of the loop body + * @param identifierInHeader collection name if loop condition is {@code i < collection.size()} or + * {@code i < arr.length} and {@code n} if loop condition is {@code i < n} + * @param iterator the name of the loop iterator variable + * @return {@code null} if the loop body writes to the iterator or collection variable; otherwise + * the last collection element access tree consistent with the loop header, if one exists + */ + private @Nullable ExpressionTree getLastElementAccessIfLoopValid( + List statements, Name identifierInHeader, Name iterator) { + AtomicBoolean blockIsIllegal = new AtomicBoolean(false); + final ExpressionTree[] collectionElementTree = {null}; + + TreeScanner scanner = + new TreeScanner() { + @Override + public Void visitUnary(UnaryTree tree, Void p) { + switch (tree.getKind()) { + case PREFIX_DECREMENT: + case POSTFIX_DECREMENT: + case PREFIX_INCREMENT: + case POSTFIX_INCREMENT: + if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getExpression()) + == iterator) { + blockIsIllegal.set(true); + } + break; + default: + break; + } + return super.visitUnary(tree, p); + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()) + == iterator) { + blockIsIllegal.set(true); + } + return super.visitCompoundAssignment(tree, p); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void p) { + Name assignedVariable = + CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()); + if (assignedVariable == iterator || assignedVariable == identifierInHeader) { + blockIsIllegal.set(true); + } + + return super.visitAssignment(tree, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + if (isIthCollectionElement(mit, iterator) + && identifierInHeader == CollectionOwnershipUtils.getNameFromExpressionTree(mit) + && identifierInHeader != null) { + collectionElementTree[0] = mit; + } + return super.visitMethodInvocation(mit, p); + } + }; + + for (StatementTree stmt : statements) { + scanner.scan(stmt, null); + } + if (!blockIsIllegal.get() && collectionElementTree[0] != null) { + return collectionElementTree[0]; + } + return null; + } + + /** + * Returns true if the given tree is of the form collection.get(i), where i is the given index + * name. + * + * @param tree the tree to check + * @param index the index variable name + * @return true if the given tree is of the form collection.get(index) + */ + private boolean isIthCollectionElement(Tree tree, Name index) { + if (tree == null || index == null) { + return false; + } + if (tree instanceof MethodInvocationTree + && index + == CollectionOwnershipUtils.getNameFromExpressionTree( + TreeUtils.getIdxForGetCall(tree))) { + MethodInvocationTree mit = (MethodInvocationTree) tree; + ExpressionTree methodSelect = mit.getMethodSelect(); + if (methodSelect instanceof MemberSelectTree) { + MemberSelectTree mst = (MemberSelectTree) methodSelect; + Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); + return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + } + } + return false; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java new file mode 100644 index 000000000000..efd8cec5c641 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -0,0 +1,870 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WhileLoopTree; +import com.sun.source.util.TreeScanner; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import javax.lang.model.element.VariableElement; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; +import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.TreeUtils; + +/** Matches `while` loops that may discharge collection obligations. */ +final class WhileDisposalLoopMatcher { + + /** The CO type factory used for collection-ownership queries. */ + private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + + /** The RLCC type factory used for declaration lookup and ignored-exception checks. */ + private final RLCCalledMethodsAnnotatedTypeFactory rlccAtf; + + /** The CFG of the method currently being scanned. */ + private final ControlFlowGraph cfg; + + /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ + private final @Nullable MethodTree methodTree; + + /** Lazily-computed CFG facts for while-loop resolution. */ + private @Nullable WhileLoopResolutionCache whileLoopCache; + + /** + * Creates a matcher for `while` disposal loops. + * + * @param atypeFactory the CO type factory + * @param rlccAtf the RLCC type factory + * @param cfg the CFG being scanned + * @param methodTree the enclosing method, or {@code null} + */ + WhileDisposalLoopMatcher( + CollectionOwnershipAnnotatedTypeFactory atypeFactory, + RLCCalledMethodsAnnotatedTypeFactory rlccAtf, + ControlFlowGraph cfg, + @Nullable MethodTree methodTree) { + this.atypeFactory = atypeFactory; + this.rlccAtf = rlccAtf; + this.cfg = cfg; + this.methodTree = methodTree; + } + + /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ + private static final class WhileLoopResolutionCache { + + /** A back edge in the CFG. */ + private static final class BlockEdge { + /** Source block of the back edge. */ + final Block sourceBlock; + + /** Target block of the back edge. */ + final Block targetBlock; + + /** + * Creates a CFG back edge description. + * + * @param sourceBlock source block of the back edge + * @param targetBlock target block of the back edge + */ + BlockEdge(Block sourceBlock, Block targetBlock) { + this.sourceBlock = sourceBlock; + this.targetBlock = targetBlock; + } + } + + /** Reachable CFG blocks in the current method. */ + private final Set reachableBlocks; + + /** Back edges among {@link #reachableBlocks}. */ + private final List backEdges; + + /** Natural loops for back edges, computed lazily. */ + private final IdentityHashMap> naturalLoopsByBackEdge = + new IdentityHashMap<>(); + + /** + * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. + * + * @param cfg the enclosing method CFG + */ + private WhileLoopResolutionCache(ControlFlowGraph cfg) { + Block entryBlock = cfg.getEntryBlock(); + this.reachableBlocks = CollectionOwnershipUtils.reachableFrom(entryBlock); + Map> dominators = computeDominators(entryBlock, reachableBlocks); + this.backEdges = findBackEdges(reachableBlocks, dominators); + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. + * + * @return the CFG back edges + */ + private List getBackEdges() { + return backEdges; + } + + /** + * Returns the natural loop induced by the given back edge, computing it lazily if needed. + * + * @param backEdge the back edge + * @return the natural loop induced by the given back edge + */ + private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { + return naturalLoopsByBackEdge.computeIfAbsent( + backEdge, + ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); + } + + /** + * Computes dominators for the reachable blocks in the current CFG. + * + * @param entryBlock the CFG entry block + * @param reachableBlocks reachable blocks in the CFG + * @return dominators for each reachable block + */ + private static Map> computeDominators( + Block entryBlock, Set reachableBlocks) { + Map> dominators = new HashMap<>(); + + for (Block block : reachableBlocks) { + if (block.equals(entryBlock)) { + dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); + } else { + dominators.put(block, new HashSet<>(reachableBlocks)); + } + } + + boolean changed; + do { + changed = false; + for (Block block : reachableBlocks) { + if (block.equals(entryBlock)) { + continue; + } + + Set newDominators = null; + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + Set predecessorDominators = dominators.get(predecessor); + if (predecessorDominators == null) { + continue; + } + if (newDominators == null) { + newDominators = new HashSet<>(predecessorDominators); + } else { + newDominators.retainAll(predecessorDominators); + } + } + + if (newDominators == null) { + newDominators = new HashSet<>(); + } + newDominators.add(block); + + if (!newDominators.equals(dominators.get(block))) { + dominators.put(block, newDominators); + changed = true; + } + } + } while (changed); + + return dominators; + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. + * + * @param reachableBlocks reachable blocks in the CFG + * @param dominators dominators for each reachable block + * @return the CFG back edges + */ + private static List findBackEdges( + Set reachableBlocks, Map> dominators) { + java.util.List backEdges = new java.util.ArrayList<>(); + for (Block sourceBlock : reachableBlocks) { + for (Block targetBlock : sourceBlock.getSuccessors()) { + if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { + continue; + } + Set sourceDominators = dominators.get(sourceBlock); + if (sourceDominators != null && sourceDominators.contains(targetBlock)) { + backEdges.add(new BlockEdge(sourceBlock, targetBlock)); + } + } + } + return backEdges; + } + + /** + * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. + * + * @param sourceBlock the source of the back edge + * @param targetBlock the target of the back edge + * @param reachableBlocks reachable blocks in the CFG + * @return the natural loop induced by the back edge + */ + private static Set naturalLoop( + Block sourceBlock, Block targetBlock, Set reachableBlocks) { + Set loopBlocks = new HashSet<>(); + ArrayDeque stack = new ArrayDeque<>(); + + loopBlocks.add(targetBlock); + if (loopBlocks.add(sourceBlock)) { + stack.push(sourceBlock); + } + + while (!stack.isEmpty()) { + Block block = stack.pop(); + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { + stack.push(predecessor); + } + } + } + return loopBlocks; + } + } + + /** + * Description of an accepted while-loop header form. + * + *

Each header form determines which extraction methods are allowed in the loop body. + */ + private static final class WhileSpec { + /** Methods that may extract an element when this header form is used. */ + final Set extractMethods; + + /** + * Creates a while-loop header specification. + * + * @param extractMethods methods that may extract an element from the looped collection + */ + WhileSpec(Set extractMethods) { + this.extractMethods = extractMethods; + } + } + + /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ + private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); + + /** + * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... + * }}, including {@code size() > 0} and {@code 0 < size()} variants. + */ + private static final WhileSpec NONEMPTY_SPEC = + new WhileSpec( + new HashSet<>( + Arrays.asList( + "poll", "pollFirst", "pollLast", "remove", "removeFirst", "removeLast", "pop"))); + + /** + * AST facts recovered from a matched while-loop header. + * + *

{@link #collectionTree} is the collection whose element obligations may be discharged. + * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link + * #collectionVarNameForBailout} names the collection variable whose writes should invalidate the + * match when present. + */ + private static final class WhileHeaderMatch { + /** Owning collection expression whose element obligations may be discharged. */ + final ExpressionTree collectionTree; + + /** Collection variable name whose writes should invalidate the match, if one exists. */ + final @Nullable Name collectionVarNameForBailout; + + /** Iterator variable or collection variable constrained by the loop header. */ + final Name headerVar; + + /** Accepted extraction shape for the matched loop header. */ + final WhileSpec spec; + + /** + * Creates a summary of the AST facts recovered from a matched while-loop header. + * + * @param collectionTree the owning collection expression to mark + * @param collectionVarNameForBailout collection variable whose writes invalidate the match + * @param headerVar iterator or collection variable constrained by the header + * @param spec accepted extraction shape for the matched loop header + */ + WhileHeaderMatch( + ExpressionTree collectionTree, + @Nullable Name collectionVarNameForBailout, + Name headerVar, + WhileSpec spec) { + this.collectionTree = collectionTree; + this.collectionVarNameForBailout = collectionVarNameForBailout; + this.headerVar = headerVar; + this.spec = spec; + } + } + + /** + * One extracted element use recovered from a while-loop body. + * + *

The extraction call is the expression that removes or advances to the next element, such as + * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. + */ + private static final class BodyExtraction { + /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ + final MethodInvocationTree extractionCall; + + /** + * Creates a body extraction summary. + * + * @param extractionCall extraction call found in the loop body + */ + BodyExtraction(MethodInvocationTree extractionCall) { + this.extractionCall = extractionCall; + } + } + + /** + * Matches a while-loop that may fulfill collection obligations. + * + *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and + * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, + * and {@code while (0 < q.size())}. + * + * @param tree the while-loop to inspect + * @return the matched disposal loop, or {@code null} if the loop does not match + */ + @Nullable DisposalLoop match(WhileLoopTree tree) { + MethodTree enclosingMethodTree = + CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); + if (enclosingMethodTree == null) { + return null; + } + ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); + WhileHeaderMatch header = matchWhileHeader(condNoParens); + if (header == null) { + return null; + } + List bodyStatements = + CollectionOwnershipUtils.getLoopBodyStatements(tree.getStatement()); + if (bodyStatements == null) { + return null; + } + BodyExtraction extraction = + findSingleExtractionInWhileBody( + bodyStatements, + header.headerVar, + header.collectionVarNameForBailout, + header.spec.extractMethods); + if (extraction == null) { + return null; + } + Block condBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, condNoParens); + if (condBlock == null) { + return null; + } + ConditionalBlock cblock = findConditionalSuccessor(condBlock); + if (cblock == null) { + Block peeled = peelExceptionBlocksToPred(condBlock); + if (peeled != null) { + cblock = findConditionalSuccessor(peeled); + } + } + if (cblock == null) { + return null; + } + + Block loopBodyEntryBlock = cblock.getThenSuccessor(); + Node elementNode = CollectionOwnershipUtils.anyNodeForTree(cfg, extraction.extractionCall); + if (elementNode == null) { + return null; + } + + Block loopUpdateBlock = + chooseLoopUpdateBlockForPotentiallyFulfillingLoop( + loopBodyEntryBlock, cblock, getOrCreateWhileLoopCache()); + if (loopUpdateBlock != null) { + return new DisposalLoop( + header.collectionTree, + extraction.extractionCall, + elementNode, + CollectionOwnershipUtils.treeForLoopCondition(cfg, condNoParens), + cblock, + loopBodyEntryBlock, + loopUpdateBlock); + } + return null; + } + + /** + * Matches supported while-loop header forms and returns the recovered loop facts. + * + *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code + * while (c.size() > 0)}, and {@code while (0 < c.size())}. + * + * @param cond the while-loop condition with parentheses removed + * @return the recovered header facts, or {@code null} if the header is unsupported + */ + private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { + if (cond instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) cond; + if (TreeUtils.isHasNextCall(mit)) { + ExpressionTree recv = receiverOfInvocation(mit); + Name itName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); + if (itName == null) { + return null; + } + ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); + if (colExpr == null) { + return null; + } + Name colName = CollectionOwnershipUtils.getNameFromExpressionTree(colExpr); + return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); + } + } + + if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { + ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); + WhileHeaderMatch m = matchNonEmptyFromExpr(inner); + if (m != null) { + return m; + } + } + + if (cond instanceof BinaryTree) { + WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); + if (m != null) { + return m; + } + } + + return null; + } + + /** + * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. + * + * @param inner the expression under the logical complement + * @return the recovered header facts, or {@code null} if the expression does not match + */ + private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { + if (!(inner instanceof MethodInvocationTree)) { + return null; + } + MethodInvocationTree mit = (MethodInvocationTree) inner; + if (!isIsEmptyCall(mit)) { + return null; + } + ExpressionTree recv = receiverOfInvocation(mit); + if (recv == null) { + return null; + } + Name varName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); + if (varName == null) { + return null; + } + Element recvElt = TreeUtils.elementFromTree(recv); + if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + return null; + } + ExpressionTree colTree = CollectionOwnershipUtils.collectionTreeFromExpression(recv); + if (colTree == null) { + return null; + } + return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + } + + /** + * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < + * c.size()}. + * + * @param condition the binary condition + * @return the recovered header facts, or {@code null} if the expression does not match + */ + private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { + Tree.Kind k = condition.getKind(); + if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { + return null; + } + + ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); + ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); + + MethodInvocationTree sizeCall = null; + com.sun.source.tree.LiteralTree zero = null; + + if (k == Tree.Kind.GREATER_THAN) { + if (left instanceof MethodInvocationTree + && right instanceof com.sun.source.tree.LiteralTree) { + sizeCall = (MethodInvocationTree) left; + zero = (com.sun.source.tree.LiteralTree) right; + } + } else { + if (left instanceof com.sun.source.tree.LiteralTree + && right instanceof MethodInvocationTree) { + zero = (com.sun.source.tree.LiteralTree) left; + sizeCall = (MethodInvocationTree) right; + } + } + + if (sizeCall == null + || !(zero.getValue() instanceof Integer) + || (Integer) zero.getValue() != 0) { + return null; + } + if (!TreeUtils.isSizeAccess(sizeCall)) { + return null; + } + + ExpressionTree recv = receiverOfInvocation(sizeCall); + if (recv == null) { + return null; + } + + Name varName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); + if (varName == null) { + return null; + } + + Element recvElt = TreeUtils.elementFromTree(recv); + if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + return null; + } + + ExpressionTree colTree = CollectionOwnershipUtils.collectionTreeFromExpression(recv); + if (colTree == null) { + return null; + } + + return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + } + + /** + * Returns whether the given invocation is an {@code isEmpty()} call with no arguments. + * + * @param invocation a method invocation + * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments + */ + private boolean isIsEmptyCall(MethodInvocationTree invocation) { + ExpressionTree sel = invocation.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) { + return false; + } + MemberSelectTree ms = (MemberSelectTree) sel; + return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); + } + + /** + * Returns the explicit receiver of the given invocation, if present. + * + * @param invocation a method invocation + * @return the explicit receiver, or {@code null} if none exists + */ + private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { + ExpressionTree sel = invocation.getMethodSelect(); + if (sel instanceof MemberSelectTree) { + return ((MemberSelectTree) sel).getExpression(); + } + return null; + } + + /** + * Recovers the collection expression from an iterator receiver in a header such as {@code while + * (it.hasNext())}. + * + *

This only recognizes local iterator variables initialized by {@code col.iterator()}. + * + * @param iteratorExpr the iterator receiver expression + * @return the collection expression, or {@code null} if it cannot be recovered + */ + private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( + ExpressionTree iteratorExpr) { + if (iteratorExpr == null) { + return null; + } + + Element itElt = TreeUtils.elementFromTree(iteratorExpr); + if (!(itElt instanceof VariableElement)) { + return null; + } + + if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { + return null; + } + + Tree decl = rlccAtf.declarationFromElement(itElt); + if (!(decl instanceof VariableTree)) { + return null; + } + + ExpressionTree init = ((VariableTree) decl).getInitializer(); + if (!(init instanceof MethodInvocationTree)) { + return null; + } + + MethodInvocationTree initCall = (MethodInvocationTree) init; + ExpressionTree sel = initCall.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) { + return null; + } + + MemberSelectTree ms = (MemberSelectTree) sel; + if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { + return null; + } + + ExpressionTree colExpr = ms.getExpression(); + Element colElt = TreeUtils.elementFromTree(colExpr); + if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { + return null; + } + + return CollectionOwnershipUtils.collectionTreeFromExpression(colExpr); + } + + /** + * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code + * null}. + * + *

This matcher rejects writes to the iterator/header variable and, when present, to the + * collection variable itself, because such writes invalidate the header/body correspondence used + * by later CFG verification. + * + * @param statements the loop body statements + * @param headerVar the iterator or collection variable constrained by the header + * @param collectionVarName the collection variable to protect from writes, if any + * @param allowedExtractMethods the extraction methods allowed by the matched header + * @return the unique extraction in the loop body, or {@code null} if the body is unsupported + */ + private @Nullable BodyExtraction findSingleExtractionInWhileBody( + List statements, + Name headerVar, + @Nullable Name collectionVarName, + Set allowedExtractMethods) { + + AtomicBoolean illegal = new AtomicBoolean(false); + final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; + final int[] extractionCount = new int[] {0}; + + TreeScanner scanner = + new TreeScanner() { + + private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { + Name assigned = CollectionOwnershipUtils.getNameFromExpressionTree(lhs); + if (assigned != null) { + if (assigned == headerVar) illegal.set(true); + if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); + } + } + + private void recordExtractionIfAny(ExpressionTree expr) { + expr = TreeUtils.withoutParens(expr); + if (!(expr instanceof MethodInvocationTree)) { + return; + } + + MethodInvocationTree mit = (MethodInvocationTree) expr; + if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { + return; + } + + extractionCount[0]++; + if (extractionCount[0] > 1) { + illegal.set(true); + return; + } + extraction[0] = mit; + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { + markWriteIfTargetsHeaderOrCollection(node.getVariable()); + return super.visitCompoundAssignment(node, p); + } + + @Override + public Void visitAssignment(AssignmentTree node, Void p) { + markWriteIfTargetsHeaderOrCollection(node.getVariable()); + recordExtractionIfAny(node.getExpression()); + return super.visitAssignment(node, p); + } + + @Override + public Void visitVariable(VariableTree vt, Void p) { + ExpressionTree init = vt.getInitializer(); + if (init != null) { + recordExtractionIfAny(init); + } + return super.visitVariable(vt, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + ExpressionTree sel = mit.getMethodSelect(); + if (sel instanceof MemberSelectTree) { + ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); + recordExtractionIfAny(recv); + } + return super.visitMethodInvocation(mit, p); + } + }; + + for (StatementTree st : statements) { + scanner.scan(st, null); + if (illegal.get()) break; + } + + if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { + return null; + } + return new BodyExtraction(extraction[0]); + } + + /** + * Returns whether the given invocation is an allowed extraction call on the matched header + * variable. + * + * @param invocation a method invocation + * @param headerVar the iterator or collection variable constrained by the header + * @param allowedExtractMethods extraction methods permitted by the matched header form + * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} + */ + private boolean isExtractionCallOnHeaderVar( + MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { + ExpressionTree sel = invocation.getMethodSelect(); + if (!(sel instanceof MemberSelectTree)) { + return false; + } + MemberSelectTree ms = (MemberSelectTree) sel; + String methodName = ms.getIdentifier().toString(); + if (!allowedExtractMethods.contains(methodName)) { + return false; + } + if (!invocation.getArguments().isEmpty()) { + return false; + } + Name recv = CollectionOwnershipUtils.getNameFromExpressionTree(ms.getExpression()); + return recv != null && recv == headerVar; + } + + /** + * Returns the conditional successor reached from the given block, if one is immediately visible. + * + * @param block a CFG block + * @return the conditional successor of {@code block}, or {@code null} if none is found + */ + private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { + for (Block succ : block.getSuccessors()) { + if (succ instanceof ConditionalBlock) { + return (ConditionalBlock) succ; + } + } + if (block instanceof SingleSuccessorBlock) { + Block succ = ((SingleSuccessorBlock) block).getSuccessor(); + if (succ instanceof ConditionalBlock) { + return (ConditionalBlock) succ; + } + } + return null; + } + + /** + * Walks backward through exception blocks to recover the predecessor block that leads to the + * actual loop conditional. + * + *

This is needed because loop conditions such as {@code iterator.hasNext()} may be represented + * by exception blocks before reaching the conditional branch. + * + * @param block a CFG block + * @return a predecessor block to retry from, or {@code null} if no such block is found + */ + private @Nullable Block peelExceptionBlocksToPred(Block block) { + Block cur = block; + Set visitedBlocks = new HashSet<>(); + while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { + Set preds = cur.getPredecessors(); + if (preds.size() != 1) { + break; + } + Block p = preds.iterator().next(); + if (p == null) { + break; + } + cur = p; + } + return cur; + } + + /** + * Chooses the best loop update block for a potentially fulfilling while loop by matching it to + * the tightest natural loop that contains both the body entry and the loop condition. + * + * @param bodyEntryBlock the loop body entry block + * @param conditionalBlock the loop conditional block + * @param whileLoopCache cached CFG facts for while-loop resolution + * @return the chosen loop update block, or {@code null} if none is found + */ + private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( + Block bodyEntryBlock, + ConditionalBlock conditionalBlock, + WhileLoopResolutionCache whileLoopCache) { + + Block bestLoopUpdateBlock = null; + int bestLoopSize = Integer.MAX_VALUE; + + for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { + Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); + + if (!naturalLoop.contains(bodyEntryBlock)) { + continue; + } + if (!naturalLoop.contains(conditionalBlock)) { + continue; + } + + if (naturalLoop.size() < bestLoopSize) { + bestLoopSize = naturalLoop.size(); + bestLoopUpdateBlock = backEdge.targetBlock; + } + } + + return bestLoopUpdateBlock; + } + + /** + * Returns the cached CFG facts for while-loop resolution, creating them lazily if needed. + * + * @return cached CFG facts for while-loop resolution + */ + private WhileLoopResolutionCache getOrCreateWhileLoopCache() { + if (whileLoopCache == null) { + whileLoopCache = new WhileLoopResolutionCache(cfg); + } + return whileLoopCache; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index b3a2d81b7321..51bc377b5c62 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -1,12 +1,10 @@ package org.checkerframework.checker.mustcall; import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; import java.lang.annotation.Annotation; @@ -38,15 +36,12 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.source.SourceChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -159,116 +154,118 @@ public MustCallAnnotatedTypeFactory(BaseTypeChecker checker) { this.postInit(); } - /** - * Records a potentially fulfilling collection loop for the given enclosing method. - * - *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - * this method does nothing. - * - * @param enclosingMethodTree the method containing the loop - * @param collectionTree the collection iterated over by the loop - * @param collectionElementTree the tree for the collection element - * @param conditionTree the loop condition - * @param loopBodyEntryBlock the CFG block for the loop body entry - * @param loopConditionalBlock the CFG conditional block for the loop - * @param collectionElementNode the CFG node for the iterated element - */ - public void recordPotentiallyFulfillingCollectionLoop( - MethodTree enclosingMethodTree, - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree conditionTree, - Block loopBodyEntryBlock, - ConditionalBlock loopConditionalBlock, - Node collectionElementNode) { - RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - if (rlccAtf == null) { - return; - } - rlccAtf.recordPotentiallyFulfillingCollectionLoop( - enclosingMethodTree, - collectionTree, - collectionElementTree, - conditionTree, - loopBodyEntryBlock, - loopConditionalBlock, - collectionElementNode); - } - - /** - * Records a potentially fulfilling enhanced-for-loop for the given enclosing method. - * - *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - * this method does nothing. - * - * @param enclosingMethodTree the method containing the loop - * @param enhancedForLoopTree the enhanced-for-loop tree - */ - public void recordPotentiallyFulfillingEnhancedForLoop( - MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { - RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - if (rlccAtf == null) { - return; - } - rlccAtf.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, enhancedForLoopTree); - } - - /** - * Records a CFG-resolved potentially fulfilling collection loop for the given enclosing method. - * - *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - * this method does nothing. - * - * @param enclosingMethodTree the method containing the loop - * @param collectionTree the collection iterated over by the loop - * @param collectionElementTree the tree for the collection element - * @param conditionTree the loop condition - * @param loopBodyEntryBlock the CFG block for the loop body entry - * @param loopUpdateBlock the CFG block for the loop update - * @param loopConditionalBlock the CFG conditional block for the loop - * @param collectionElementNode the CFG node for the iterated element - */ - public void recordResolvedPotentiallyFulfillingCollectionLoop( - MethodTree enclosingMethodTree, - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree conditionTree, - Block loopBodyEntryBlock, - Block loopUpdateBlock, - ConditionalBlock loopConditionalBlock, - Node collectionElementNode) { - RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - if (rlccAtf == null) { - return; - } - rlccAtf.recordResolvedPotentiallyFulfillingCollectionLoop( - enclosingMethodTree, - collectionTree, - collectionElementTree, - conditionTree, - loopBodyEntryBlock, - loopUpdateBlock, - loopConditionalBlock, - collectionElementNode); - } - - /** - * Returns the RLCC called-methods type factory if this factory is part of the Resource Leak - * Checker hierarchy, or {@code null} otherwise. - * - * @return the RLCC called-methods type factory, or {@code null} - */ - private @Nullable RLCCalledMethodsAnnotatedTypeFactory getRlccAtfIfPartOfHierarchy() { - SourceChecker currentChecker = checker; - while (currentChecker != null) { - String currentCheckerName = currentChecker.getClass().getCanonicalName(); - if (ResourceLeakUtils.rlcCheckers.contains(currentCheckerName)) { - return ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); - } - currentChecker = currentChecker.getParentChecker(); - } - return null; - } + // /** + // * Records a potentially fulfilling collection loop for the given enclosing method. + // * + // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + // * this method does nothing. + // * + // * @param enclosingMethodTree the method containing the loop + // * @param collectionTree the collection iterated over by the loop + // * @param collectionElementTree the tree for the collection element + // * @param conditionTree the loop condition + // * @param loopBodyEntryBlock the CFG block for the loop body entry + // * @param loopConditionalBlock the CFG conditional block for the loop + // * @param collectionElementNode the CFG node for the iterated element + // */ + // public void recordPotentiallyFulfillingCollectionLoop( + // MethodTree enclosingMethodTree, + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree conditionTree, + // Block loopBodyEntryBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionElementNode) { + // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + // if (rlccAtf == null) { + // return; + // } + // rlccAtf.recordPotentiallyFulfillingCollectionLoop( + // enclosingMethodTree, + // collectionTree, + // collectionElementTree, + // conditionTree, + // loopBodyEntryBlock, + // loopConditionalBlock, + // collectionElementNode); + // } + // + // /** + // * Records a potentially fulfilling enhanced-for-loop for the given enclosing method. + // * + // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + // * this method does nothing. + // * + // * @param enclosingMethodTree the method containing the loop + // * @param enhancedForLoopTree the enhanced-for-loop tree + // */ + // public void recordPotentiallyFulfillingEnhancedForLoop( + // MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { + // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + // if (rlccAtf == null) { + // return; + // } + // rlccAtf.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, + // enhancedForLoopTree); + // } + // + // /** + // * Records a CFG-resolved potentially fulfilling collection loop for the given enclosing + // method. + // * + // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then + // * this method does nothing. + // * + // * @param enclosingMethodTree the method containing the loop + // * @param collectionTree the collection iterated over by the loop + // * @param collectionElementTree the tree for the collection element + // * @param conditionTree the loop condition + // * @param loopBodyEntryBlock the CFG block for the loop body entry + // * @param loopUpdateBlock the CFG block for the loop update + // * @param loopConditionalBlock the CFG conditional block for the loop + // * @param collectionElementNode the CFG node for the iterated element + // */ + // public void recordResolvedPotentiallyFulfillingCollectionLoop( + // MethodTree enclosingMethodTree, + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree conditionTree, + // Block loopBodyEntryBlock, + // Block loopUpdateBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionElementNode) { + // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); + // if (rlccAtf == null) { + // return; + // } + // rlccAtf.recordResolvedPotentiallyFulfillingCollectionLoop( + // enclosingMethodTree, + // collectionTree, + // collectionElementTree, + // conditionTree, + // loopBodyEntryBlock, + // loopUpdateBlock, + // loopConditionalBlock, + // collectionElementNode); + // } + + // /** + // * Returns the RLCC called-methods type factory if this factory is part of the Resource Leak + // * Checker hierarchy, or {@code null} otherwise. + // * + // * @return the RLCC called-methods type factory, or {@code null} + // */ + // private @Nullable RLCCalledMethodsAnnotatedTypeFactory getRlccAtfIfPartOfHierarchy() { + // SourceChecker currentChecker = checker; + // while (currentChecker != null) { + // String currentCheckerName = currentChecker.getClass().getCanonicalName(); + // if (ResourceLeakUtils.rlcCheckers.contains(currentCheckerName)) { + // return ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); + // } + // currentChecker = currentChecker.getParentChecker(); + // } + // return null; + // } @Override public void setRoot(@Nullable CompilationUnitTree newRoot) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index 8d8f488439c4..f27ba56c9e94 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -2,40 +2,21 @@ import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.AssignmentTree; -import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.CompoundAssignmentTree; -import com.sun.source.tree.EnhancedForLoopTree; -import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.ForLoopTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.UnaryTree; -import com.sun.source.tree.VariableTree; -import com.sun.source.tree.WhileLoopTree; -import com.sun.source.util.TreeScanner; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; 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.Name; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.mustcall.qual.InheritableMustCall; import org.checkerframework.checker.mustcall.qual.MustCall; @@ -43,15 +24,8 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.mustcall.qual.PolyMustCall; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; -import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -356,1053 +330,1073 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { return null; } - /** - * Syntactically matches indexed for-loops that iterate over all elements of a collection. - * - *

This logic lives in the Must Call visitor because matching must complete before collection - * ownership transfer runs. - */ - @Override - public Void visitForLoop(ForLoopTree tree, Void p) { - boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; - if (singleLoopVariable) { - detectCollectionObligationFulfillingLoop(tree); - } - return super.visitForLoop(tree, p); - } - - /** - * Performs AST-only matching for while-loops that may fulfill collection obligations. - * - *

RLCC resolves the remaining CFG-local loop facts later, during post-analysis of the - * enclosing method. - */ - @Override - public Void visitWhileLoop(WhileLoopTree tree, Void p) { - detectCollectionObligationFulfillingWhileLoop(tree); - return super.visitWhileLoop(tree, p); - } - - /** - * Performs AST-only matching for enhanced-for-loops that may fulfill collection obligations. - * - *

The visitor records only the loop tree here. RLCC resolves the desugared iterator CFG shape - * later during post-analysis of the enclosing method. - * - * @param tree the enhanced-for-loop to inspect - */ - @Override - public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { - detectPotentiallyFulfillingEnhancedForLoop(tree); - return super.visitEnhancedForLoop(tree, p); - } - - /** - * Records an enhanced-for-loop that potentially fulfills collection obligations. - * - *

This method only performs AST matching. RLCC resolves the CFG-specific loop facts later, - * during post-analysis of the enclosing method. - * - * @param tree the enhanced-for-loop to inspect - */ - private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree) { - MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - if (enclosingMethodTree == null) { - return; - } - ExpressionTree collectionTree = collectionTreeFromExpression(tree.getExpression()); - if (collectionTree == null) { - return; - } - if (!ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(atypeFactory) - .isResourceCollection(collectionTree)) { - return; - } - atypeFactory.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, tree); - } - - /** - * Description of an accepted while-loop header form. - * - *

Each header form determines which extraction methods are allowed in the loop body. - */ - private static final class WhileSpec { - /** Methods that may extract an element when this header form is used. */ - final Set extractMethods; - - /** - * Creates a while-loop header specification. - * - * @param extractMethods methods that may extract an element from the looped collection - */ - WhileSpec(Set extractMethods) { - this.extractMethods = extractMethods; - } - } - - /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ - private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); - - /** - * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... - * }}, including {@code size() > 0} and {@code 0 < size()} variants. - */ - private static final WhileSpec NONEMPTY_SPEC = - new WhileSpec( - new HashSet<>( - Arrays.asList( - // Queue/Deque (null-returning) - "poll", - "pollFirst", - "pollLast", - // Deque (throws on empty, but guarded by condition) - "remove", - "removeFirst", - "removeLast", - // Stack - "pop"))); - - /** - * AST facts recovered from a matched while-loop header. - * - *

{@link #collectionTree} is the collection whose element obligations may be discharged. - * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link - * #collectionVarNameForBailout} names the collection variable whose writes should invalidate the - * match when present. - */ - private static final class WhileHeaderMatch { - /** Owning collection expression whose element obligations may be discharged. */ - final ExpressionTree collectionTree; - - /** Collection variable name whose writes should invalidate the match, if one exists. */ - final @Nullable Name collectionVarNameForBailout; - - /** Iterator variable or collection variable constrained by the loop header. */ - final Name headerVar; - - /** Accepted extraction shape for the matched loop header. */ - final WhileSpec spec; - - /** - * Creates a summary of the AST facts recovered from a matched while-loop header. - * - * @param collectionTree the owning collection expression to mark - * @param collectionVarNameForBailout collection variable whose writes invalidate the match - * @param headerVar iterator or collection variable constrained by the header - * @param spec accepted extraction shape for the matched loop header - */ - WhileHeaderMatch( - ExpressionTree collectionTree, - @Nullable Name collectionVarNameForBailout, - Name headerVar, - WhileSpec spec) { - this.collectionTree = collectionTree; - this.collectionVarNameForBailout = collectionVarNameForBailout; - this.headerVar = headerVar; - this.spec = spec; - } - } - - /** - * Records a while-loop that may fulfill collection obligations. - * - *

This method performs AST matching plus the small amount of CFG lookup needed to identify the - * condition block, the conditional successor, the body entry block, and the extracted element - * node. RLCC resolves the remaining CFG-local fact, the loop update block, later during - * post-analysis. - * - *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and - * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, - * and {@code while (0 < q.size())}. - * - * @param tree the while-loop to inspect - */ - private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { - MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - if (enclosingMethodTree == null) { - return; - } - // 1) Match header - ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); - WhileHeaderMatch header = matchWhileHeader(condNoParens); - if (header == null) { - return; - } - // 2) Extract body statements. - List bodyStatements = getLoopBodyStatements(tree.getStatement()); - if (bodyStatements == null) { - return; - } - // 3) Find exactly one extraction call in the body to be sound. - BodyExtraction extraction = - findSingleExtractionInWhileBody( - bodyStatements, - header.headerVar, - header.collectionVarNameForBailout, - header.spec.extractMethods); - if (extraction == null) { - return; - } - // Resolve CFG-local metadata (except loopUpdateBlock). - Block condBlock = firstBlockForTree(condNoParens); - if (condBlock == null) { - return; - } - // Find the ConditionalBlock that branches on the while condition. - ConditionalBlock cblock = findConditionalSuccessor(condBlock); - if (cblock == null) { - // condition often lives in ExceptionBlocks; try walking up preds and retry - Block peeled = peelExceptionBlocksToPred(condBlock); - if (peeled != null) { - cblock = findConditionalSuccessor(peeled); - } - } - if (cblock == null) { - return; - } - - Block loopBodyEntryBlock = cblock.getThenSuccessor(); - - // Node for the extraction call tree (it.next()/q.poll()/s.pop()). - Node elementNode = anyNodeForTree(extraction.extractionCall); - if (elementNode == null) { - return; - } - - // 4) Record a potentially fulfilling collection loop. - // - // Store: - // - collectionTree (resources / q / s) - // - collectionElementTree (it.next() / q.poll() / s.pop()) - // - condition tree (the while condition) - atypeFactory.recordPotentiallyFulfillingCollectionLoop( - enclosingMethodTree, - header.collectionTree, - extraction.extractionCall, - condNoParens, - loopBodyEntryBlock, - cblock, - elementNode); - } - - /** - * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a - * lambda expression. - * - *

The per-method loop-state refactor records only loops that are part of the enclosing method - * analysis. Lambda-local loop support can be added separately if needed. - * - * @return the enclosing method for the current loop, or {@code null} if it is inside a lambda - */ - private @Nullable MethodTree getEnclosingMethodForCollectionLoop() { - Tree enclosingMethodOrLambda = TreePathUtil.enclosingMethodOrLambda(getCurrentPath()); - if (enclosingMethodOrLambda instanceof MethodTree) { - return (MethodTree) enclosingMethodOrLambda; - } - return null; - } - - /** - * Returns the statements in a loop body, regardless of whether the body is a block. - * - * @param statement the loop body statement - * @return the loop body statements, or {@code null} if {@code statement} is {@code null} - */ - private @Nullable List getLoopBodyStatements( - @Nullable StatementTree statement) { - if (statement == null) { - return null; - } - return statement instanceof BlockTree - ? ((BlockTree) statement).getStatements() - : Collections.singletonList(statement); - } - - /** - * Returns the first CFG block associated with the given tree. - * - * @param tree a tree - * @return the first CFG block associated with {@code tree}, or {@code null} if none is known - */ - private @Nullable Block firstBlockForTree(Tree tree) { - Set nodes = atypeFactory.getNodesForTree(tree); - if (nodes == null || nodes.isEmpty()) { - return null; - } - for (Node n : nodes) { - Block block = n.getBlock(); - if (block != null) { - return block; - } - } - return null; - } - - /** - * Returns an arbitrary CFG node associated with the given tree. - * - * @param tree a tree - * @return a CFG node associated with {@code tree}, or {@code null} if none is known - */ - private @Nullable Node anyNodeForTree(Tree tree) { - Set nodes = atypeFactory.getNodesForTree(tree); - if (nodes == null || nodes.isEmpty()) { - return null; - } - return nodes.iterator().next(); - } - - /** - * Returns the conditional successor reached from the given block, if one is immediately visible. - * - * @param block a CFG block - * @return the conditional successor of {@code block}, or {@code null} if none is found - */ - private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { - for (Block succ : block.getSuccessors()) { - if (succ instanceof ConditionalBlock) { - return (ConditionalBlock) succ; - } - } - if (block instanceof SingleSuccessorBlock) { - Block succ = ((SingleSuccessorBlock) block).getSuccessor(); - if (succ instanceof ConditionalBlock) { - return (ConditionalBlock) succ; - } - } - return null; - } - - /** - * Walks backward through exception blocks to recover the predecessor block that leads to the - * actual loop conditional. - * - *

This is needed because loop conditions such as {@code iterator.hasNext()} may be represented - * by exception blocks before reaching the conditional branch. - * - * @param block a CFG block - * @return a predecessor block to retry from, or {@code null} if no such block is found - */ - private @Nullable Block peelExceptionBlocksToPred(Block block) { - Block cur = block; - Set visitedBlocks = new HashSet<>(); - while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { - Set preds = cur.getPredecessors(); - if (preds.size() != 1) { - break; - } - Block p = preds.iterator().next(); - if (p == null) { - break; - } - cur = p; - } - return cur; - } - - /** - * Matches supported while-loop header forms and returns the recovered loop facts. - * - *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code - * while (c.size() > 0)}, and {@code while (0 < c.size())}. - * - * @param cond the while-loop condition with parentheses removed - * @return the recovered header facts, or {@code null} if the header is unsupported - */ - private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { - // Case A: while (it.hasNext()) - if (cond instanceof MethodInvocationTree) { - MethodInvocationTree mit = (MethodInvocationTree) cond; - if (TreeUtils.isHasNextCall(mit)) { - ExpressionTree recv = receiverOfInvocation(mit); - Name itName = getNameFromExpressionTree(recv); - if (itName == null) { - return null; - } - ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); - if (colExpr == null) { - return null; - } - Name colName = getNameFromExpressionTree(colExpr); - return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); - } - } - - // Case B1: while (!c.isEmpty()) - if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { - ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); - WhileHeaderMatch m = matchNonEmptyFromExpr(inner); - if (m != null) { - return m; - } - } - - // Case B2: while (c.size() > 0) or while (0 < c.size()) - if (cond instanceof BinaryTree) { - WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); - if (m != null) { - return m; - } - } - - return null; - } - - /** - * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. - * - * @param inner the expression under the logical complement - * @return the recovered header facts, or {@code null} if the expression does not match - */ - private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { - if (!(inner instanceof MethodInvocationTree)) { - return null; - } - MethodInvocationTree mit = (MethodInvocationTree) inner; - if (!isIsEmptyCall(mit)) { - return null; - } - ExpressionTree recv = receiverOfInvocation(mit); - if (recv == null) { - return null; - } - Name varName = getNameFromExpressionTree(recv); - if (varName == null) { - return null; - } - Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { - return null; - } - ExpressionTree colTree = collectionTreeFromExpression(recv); - if (colTree == null) { - return null; - } - return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); - } - - /** - * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < - * c.size()}. - * - * @param condition the binary condition - * @return the recovered header facts, or {@code null} if the expression does not match - */ - private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { - Tree.Kind k = condition.getKind(); - if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { - return null; - } - - ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); - ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); - - // Normalize: accept "c.size() > 0" or "0 < c.size()" - MethodInvocationTree sizeCall = null; - LiteralTree zero = null; - - if (k == Tree.Kind.GREATER_THAN) { - // left must be size(), right must be 0 - if (left instanceof MethodInvocationTree && right instanceof LiteralTree) { - sizeCall = (MethodInvocationTree) left; - zero = (LiteralTree) right; - } - } else { // LESS_THAN - // left must be 0, right must be size() - if (left instanceof LiteralTree && right instanceof MethodInvocationTree) { - zero = (LiteralTree) left; - sizeCall = (MethodInvocationTree) right; - } - } - - if (sizeCall == null - || !(zero.getValue() instanceof Integer) - || (Integer) zero.getValue() != 0) { - return null; - } - if (!TreeUtils.isSizeAccess(sizeCall)) { - return null; - } - - ExpressionTree recv = receiverOfInvocation(sizeCall); - if (recv == null) { - return null; - } - - Name varName = getNameFromExpressionTree(recv); - if (varName == null) { - return null; - } - - Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { - return null; - } - - ExpressionTree colTree = collectionTreeFromExpression(recv); - if (colTree == null) { - return null; - } - - return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); - } - - /** - * Returns whether the given invocation is an {@code isEmpty()} call with no arguments. - * - * @param invocation a method invocation - * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments - */ - private boolean isIsEmptyCall(MethodInvocationTree invocation) { - ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { - return false; - } - MemberSelectTree ms = (MemberSelectTree) sel; - return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); - } - - /** - * Returns the explicit receiver of the given invocation, if present. - * - * @param invocation a method invocation - * @return the explicit receiver, or {@code null} if none exists - */ - private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { - ExpressionTree sel = invocation.getMethodSelect(); - if (sel instanceof MemberSelectTree) { - return ((MemberSelectTree) sel).getExpression(); - } - return null; - } - - /** - * Recovers the collection expression from an iterator receiver in a header such as {@code while - * (it.hasNext())}. - * - *

This only recognizes local iterator variables initialized by {@code col.iterator()}. - * - * @param iteratorExpr the iterator receiver expression - * @return the collection expression, or {@code null} if it cannot be recovered - */ - private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( - ExpressionTree iteratorExpr) { - if (iteratorExpr == null) { - return null; - } - - Element itElt = TreeUtils.elementFromTree(iteratorExpr); - if (!(itElt instanceof VariableElement)) { - return null; - } - - // Only recover from local variable declaration with initializer "col.iterator()" - if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { - return null; - } - - Tree decl = atypeFactory.declarationFromElement(itElt); - if (!(decl instanceof VariableTree)) { - return null; - } - - ExpressionTree init = ((VariableTree) decl).getInitializer(); - if (!(init instanceof MethodInvocationTree)) { - return null; - } - - MethodInvocationTree initCall = (MethodInvocationTree) init; - ExpressionTree sel = initCall.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { - return null; - } - - MemberSelectTree ms = (MemberSelectTree) sel; - if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { - return null; - } - - ExpressionTree colExpr = ms.getExpression(); - Element colElt = TreeUtils.elementFromTree(colExpr); - if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { - return null; - } - - return collectionTreeFromExpression(colExpr); - } - - /** - * One extracted element use recovered from a while-loop body. - * - *

The extraction call is the expression that removes or advances to the next element, such as - * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. - */ - private static final class BodyExtraction { - /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ - final MethodInvocationTree extractionCall; - - /** - * Creates a body extraction summary. - * - * @param extractionCall extraction call found in the loop body - */ - BodyExtraction(MethodInvocationTree extractionCall) { - this.extractionCall = extractionCall; - } - } - - /** - * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code - * null}. - * - *

This matcher rejects writes to the iterator/header variable and, when present, to the - * collection variable itself, because such writes invalidate the header/body correspondence used - * by later CFG verification. - * - * @param statements the loop body statements - * @param headerVar the iterator or collection variable constrained by the header - * @param collectionVarName the collection variable to protect from writes, if any - * @param allowedExtractMethods the extraction methods allowed by the matched header - * @return the unique extraction in the loop body, or {@code null} if the body is unsupported - */ - private @Nullable BodyExtraction findSingleExtractionInWhileBody( - List statements, - Name headerVar, - @Nullable Name collectionVarName, - Set allowedExtractMethods) { - - AtomicBoolean illegal = new AtomicBoolean(false); - final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; - final int[] extractionCount = new int[] {0}; - - TreeScanner scanner = - new TreeScanner() { - - private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { - Name assigned = getNameFromExpressionTree(lhs); - if (assigned != null) { - if (assigned == headerVar) illegal.set(true); - if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); - } - } - - private void recordExtractionIfAny(ExpressionTree expr) { - expr = TreeUtils.withoutParens(expr); - if (!(expr instanceof MethodInvocationTree)) { - return; - } - - MethodInvocationTree mit = (MethodInvocationTree) expr; - if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { - return; - } - - extractionCount[0]++; - if (extractionCount[0] > 1) { - illegal.set(true); - return; - } - extraction[0] = mit; - } - - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - markWriteIfTargetsHeaderOrCollection(node.getVariable()); - return super.visitCompoundAssignment(node, p); - } - - @Override - public Void visitAssignment(AssignmentTree node, Void p) { - markWriteIfTargetsHeaderOrCollection(node.getVariable()); - recordExtractionIfAny(node.getExpression()); // r = it.next() - return super.visitAssignment(node, p); - } - - @Override - public Void visitVariable(VariableTree vt, Void p) { - ExpressionTree init = vt.getInitializer(); - if (init != null) { - recordExtractionIfAny(init); // T r = it.next() - } - return super.visitVariable(vt, p); - } - - @Override - public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - // Direct use: it.next().close() => receiver is it.next() - ExpressionTree sel = mit.getMethodSelect(); - if (sel instanceof MemberSelectTree) { - ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); - recordExtractionIfAny(recv); - } - return super.visitMethodInvocation(mit, p); - } - }; - - for (StatementTree st : statements) { - scanner.scan(st, null); - if (illegal.get()) break; - } - - if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { - return null; - } - return new BodyExtraction(extraction[0]); - } - - /** - * Returns whether the given invocation is an allowed extraction call on the matched header - * variable. - * - * @param invocation a method invocation - * @param headerVar the iterator or collection variable constrained by the header - * @param allowedExtractMethods extraction methods permitted by the matched header form - * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} - */ - private boolean isExtractionCallOnHeaderVar( - MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { - ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { - return false; - } - MemberSelectTree ms = (MemberSelectTree) sel; - String methodName = ms.getIdentifier().toString(); - if (!allowedExtractMethods.contains(methodName)) { - return false; - } - if (!invocation.getArguments().isEmpty()) { - return false; - } - Name recv = getNameFromExpressionTree(ms.getExpression()); - return recv != null && recv == headerVar; - } - - /** - * Marks the for-loop if it potentially fulfills collection obligations of a collection. - * - * @param tree a `for` loop with exactly one loop variable - */ - private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { - MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - if (enclosingMethodTree == null) { - return; - } - - List loopBodyStatements = getLoopBodyStatements(tree.getStatement()); - if (loopBodyStatements == null) { - return; - } - StatementTree init = tree.getInitializer().get(0); - ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); - ExpressionStatementTree update = tree.getUpdate().get(0); - if (!(condition instanceof BinaryTree)) { - return; - } - Name identifierInHeader = - nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); - Name iterator = getNameFromStatementTree(init); - if (identifierInHeader == null || iterator == null) { - return; - } - ExpressionTree collectionElementTree = - getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); - if (collectionElementTree != null) { - // Pattern match succeeded, now mark the loop in the respective datastructures. - - Block loopConditionBlock = null; - for (Node node : atypeFactory.getNodesForTree(condition)) { - Block blockOfNode = node.getBlock(); - if (blockOfNode != null) { - loopConditionBlock = blockOfNode; - break; - } - } - - Block loopUpdateBlock = null; - for (Node node : atypeFactory.getNodesForTree(update.getExpression())) { - Block blockOfNode = node.getBlock(); - if (blockOfNode != null) { - loopUpdateBlock = blockOfNode; - break; - } - } - - Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); - Node nodeForCollectionElt = null; - if (collectionEltNodes != null) { - nodeForCollectionElt = collectionEltNodes.iterator().next(); - } - if (loopUpdateBlock == null || loopConditionBlock == null) { - return; - } - // Record the loop in the RLCCalledMethods ATF's per-method loop state so that it can - // analyze it later. - // MustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop will then - // add verified fulfilling loops to the collection-ownership ATF. - Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); - Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); - atypeFactory.recordResolvedPotentiallyFulfillingCollectionLoop( - enclosingMethodTree, - collectionTreeFromExpression(collectionElementTree), - collectionElementTree, - tree.getCondition(), - loopBodyEntryBlock, - loopUpdateBlock, - (ConditionalBlock) conditionalBlock, - nodeForCollectionElt); - } - } - - /** - * Conservatively decides whether a loop iterates over all elements of some collection, using the - * following rules: - * - *

    - *
  • only one loop variable - *
  • initialization must be of the form i = 0 - *
  • condition must be of the form (i < col.size()) - *
  • update must be prefix or postfix {@code ++} - *
- * - * Returns: - * - *
    - *
  • null, if any of the above rules is violated - *
  • the name of the collection if the loop condition is of the form (i < col.size()) - *
- * - * @param init the initializer of the loop - * @param condition the loop condition - * @param update the loop update - * @return the name of the collection that the loop iterates over all elements of, or null - */ - protected Name nameOfCollectionThatAllElementsAreCalledOn( - StatementTree init, BinaryTree condition, ExpressionStatementTree update) { - Tree.Kind updateKind = update.getExpression().getKind(); - if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { - UnaryTree inc = (UnaryTree) update.getExpression(); - - // Verify update is of form i++ or ++i and init is variable initializer. - if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) - return null; - VariableTree initVar = (VariableTree) init; - - // Verify that intializer is i=0. - if (!(initVar.getInitializer() instanceof LiteralTree) - || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { - return null; - } - - // Verify that condition is of the form: i < something. - if (!(condition.getLeftOperand() instanceof IdentifierTree)) { - return null; - } - - // Verify that i=0, i statements, Name identifierInHeader, Name iterator) { - AtomicBoolean blockIsIllegal = new AtomicBoolean(false); - final ExpressionTree[] collectionElementTree = {null}; - - TreeScanner scanner = - new TreeScanner() { - @Override - public Void visitUnary(UnaryTree tree, Void p) { - switch (tree.getKind()) { - case PREFIX_DECREMENT: - case POSTFIX_DECREMENT: - case PREFIX_INCREMENT: - case POSTFIX_INCREMENT: - if (getNameFromExpressionTree(tree.getExpression()) == iterator) { - blockIsIllegal.set(true); - } - break; - default: - break; - } - return super.visitUnary(tree, p); - } - - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { - if (getNameFromExpressionTree(tree.getVariable()) == iterator) { - blockIsIllegal.set(true); - } - return super.visitCompoundAssignment(tree, p); - } - - @Override - public Void visitAssignment(AssignmentTree tree, Void p) { - Name assignedVariable = getNameFromExpressionTree(tree.getVariable()); - if (assignedVariable == iterator || assignedVariable == identifierInHeader) { - blockIsIllegal.set(true); - } - - return super.visitAssignment(tree, p); - } - - // check whether corresponds to collection.get(i) - @Override - public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - if (isIthCollectionElement(mit, iterator) - && identifierInHeader == getNameFromExpressionTree(mit) - && identifierInHeader != null) { - collectionElementTree[0] = mit; - } - return super.visitMethodInvocation(mit, p); - } - }; - - for (StatementTree stmt : statements) { - scanner.scan(stmt, null); - } - if (!blockIsIllegal.get() && collectionElementTree[0] != null) { - return collectionElementTree[0]; - } - return null; - } - - /** - * Returns the simple name of the identifier referenced by the given expression, or {@code null} - * if the expression does not reference an identifier. - * - * @param expr an expression - * @return the name of the referenced identifier, or {@code null} if none - */ - protected Name getNameFromExpressionTree(ExpressionTree expr) { - if (expr == null) { - return null; - } - switch (expr.getKind()) { - case IDENTIFIER: - return ((IdentifierTree) expr).getName(); - case MEMBER_SELECT: - MemberSelectTree mst = (MemberSelectTree) expr; - Element elt = TreeUtils.elementFromUse(mst); - if (elt.getKind() == ElementKind.FIELD) { - // this.files ==> "files" (NOT "this") - return mst.getIdentifier(); - } else if (elt.getKind() == ElementKind.METHOD) { - // resources.size ==> "resources" - return getNameFromExpressionTree(mst.getExpression()); - } else { - return null; - } - case METHOD_INVOCATION: - return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); - default: - return null; - } - } - - /** - * Returns the simple name of the identifier declared or referenced by the given statement, or - * {@code null} if the statement does not declare or reference an identifier. - * - * @param expr the {@code StatementTree} - * @return the name of the identifier declared or referenced by the statement, or {@code null} if - * none - */ - protected Name getNameFromStatementTree(StatementTree expr) { - if (expr == null) { - return null; - } - switch (expr.getKind()) { - case VARIABLE: - return ((VariableTree) expr).getName(); - case EXPRESSION_STATEMENT: - return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); - default: - return null; - } - } - - /** - * Returns the ExpressionTree of the collection in the given expression. - * - * @param expr ExpressionTree - * @return the expression evaluates to or null if it doesn't - */ - protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { - switch (expr.getKind()) { - case IDENTIFIER: - return expr; - case MEMBER_SELECT: - MemberSelectTree mst = (MemberSelectTree) expr; - Element elt = TreeUtils.elementFromUse(mst); - if (elt.getKind() == ElementKind.METHOD) { - return ((MemberSelectTree) expr).getExpression(); - } else if (elt.getKind() == ElementKind.FIELD) { - return expr; - } else { - return null; - } - case METHOD_INVOCATION: - return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); - default: - return null; - } - } - - /** - * Returns true if the given tree is of the form collection.get(i), where i is the given index - * name. - * - * @param tree the tree to check - * @param index the index variable name - * @return true if the given tree is of the form collection.get(index) - */ - private boolean isIthCollectionElement(Tree tree, Name index) { - if (tree == null || index == null) { - return false; - } - if (tree instanceof MethodInvocationTree - && index == getNameFromExpressionTree(TreeUtils.getIdxForGetCall(tree))) { - MethodInvocationTree mit = (MethodInvocationTree) tree; - ExpressionTree methodSelect = mit.getMethodSelect(); - if (methodSelect instanceof MemberSelectTree) { - MemberSelectTree mst = (MemberSelectTree) methodSelect; - Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); - return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); - } - } - return false; - } + // /** + // * Syntactically matches indexed for-loops that iterate over all elements of a collection. + // * + // *

This logic lives in the Must Call visitor because matching must complete before + // collection + // * ownership transfer runs. + // */ + // @Override + // public Void visitForLoop(ForLoopTree tree, Void p) { + // boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == + // 1; + // if (singleLoopVariable) { + // detectCollectionObligationFulfillingLoop(tree); + // } + // return super.visitForLoop(tree, p); + // } + // + //// /** + //// * Performs AST-only matching for while-loops that may fulfill collection obligations. + //// * + //// *

RLCC resolves the remaining CFG-local loop facts later, during post-analysis of the + //// * enclosing method. + //// */ + //// @Override + //// public Void visitWhileLoop(WhileLoopTree tree, Void p) { + //// detectCollectionObligationFulfillingWhileLoop(tree); + //// return super.visitWhileLoop(tree, p); + //// } + // + // /** + // * Performs AST-only matching for enhanced-for-loops that may fulfill collection obligations. + // * + // *

The visitor records only the loop tree here. RLCC resolves the desugared iterator CFG + // shape + // * later during post-analysis of the enclosing method. + // * + // * @param tree the enhanced-for-loop to inspect + // */ + // @Override + // public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { + // detectPotentiallyFulfillingEnhancedForLoop(tree); + // return super.visitEnhancedForLoop(tree, p); + // } + + // /** + // * Records an enhanced-for-loop that potentially fulfills collection obligations. + // * + // *

This method only performs AST matching. RLCC resolves the CFG-specific loop facts later, + // * during post-analysis of the enclosing method. + // * + // * @param tree the enhanced-for-loop to inspect + // */ + // private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree) { + // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + // if (enclosingMethodTree == null) { + // return; + // } + // ExpressionTree collectionTree = collectionTreeFromExpression(tree.getExpression()); + // if (collectionTree == null) { + // return; + // } + // if (!ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(atypeFactory) + // .isResourceCollection(collectionTree)) { + // return; + // } + // atypeFactory.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, tree); + // } + + // /** + // * Description of an accepted while-loop header form. + // * + // *

Each header form determines which extraction methods are allowed in the loop body. + // */ + // private static final class WhileSpec { + // /** Methods that may extract an element when this header form is used. */ + // final Set extractMethods; + // + // /** + // * Creates a while-loop header specification. + // * + // * @param extractMethods methods that may extract an element from the looped collection + // */ + // WhileSpec(Set extractMethods) { + // this.extractMethods = extractMethods; + // } + // } + // + // /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ + // private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); + // + // /** + // * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... + // ... + // * }}, including {@code size() > 0} and {@code 0 < size()} variants. + // */ + // private static final WhileSpec NONEMPTY_SPEC = + // new WhileSpec( + // new HashSet<>( + // Arrays.asList( + // // Queue/Deque (null-returning) + // "poll", + // "pollFirst", + // "pollLast", + // // Deque (throws on empty, but guarded by condition) + // "remove", + // "removeFirst", + // "removeLast", + // // Stack + // "pop"))); + // + // /** + // * AST facts recovered from a matched while-loop header. + // * + // *

{@link #collectionTree} is the collection whose element obligations may be discharged. + // * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link + // * #collectionVarNameForBailout} names the collection variable whose writes should invalidate + // the + // * match when present. + // */ + // private static final class WhileHeaderMatch { + // /** Owning collection expression whose element obligations may be discharged. */ + // final ExpressionTree collectionTree; + // + // /** Collection variable name whose writes should invalidate the match, if one exists. */ + // final @Nullable Name collectionVarNameForBailout; + // + // /** Iterator variable or collection variable constrained by the loop header. */ + // final Name headerVar; + // + // /** Accepted extraction shape for the matched loop header. */ + // final WhileSpec spec; + // + // /** + // * Creates a summary of the AST facts recovered from a matched while-loop header. + // * + // * @param collectionTree the owning collection expression to mark + // * @param collectionVarNameForBailout collection variable whose writes invalidate the match + // * @param headerVar iterator or collection variable constrained by the header + // * @param spec accepted extraction shape for the matched loop header + // */ + // WhileHeaderMatch( + // ExpressionTree collectionTree, + // @Nullable Name collectionVarNameForBailout, + // Name headerVar, + // WhileSpec spec) { + // this.collectionTree = collectionTree; + // this.collectionVarNameForBailout = collectionVarNameForBailout; + // this.headerVar = headerVar; + // this.spec = spec; + // } + // } + // + // /** + // * Records a while-loop that may fulfill collection obligations. + // * + // *

This method performs AST matching plus the small amount of CFG lookup needed to identify + // the + // * condition block, the conditional successor, the body entry block, and the extracted element + // * node. RLCC resolves the remaining CFG-local fact, the loop update block, later during + // * post-analysis. + // * + // *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and + // * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > + // 0)}, + // * and {@code while (0 < q.size())}. + // * + // * @param tree the while-loop to inspect + // */ + // private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { + // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + // if (enclosingMethodTree == null) { + // return; + // } + // // 1) Match header + // ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); + // WhileHeaderMatch header = matchWhileHeader(condNoParens); + // if (header == null) { + // return; + // } + // // 2) Extract body statements. + // List bodyStatements = getLoopBodyStatements(tree.getStatement()); + // if (bodyStatements == null) { + // return; + // } + // // 3) Find exactly one extraction call in the body to be sound. + // BodyExtraction extraction = + // findSingleExtractionInWhileBody( + // bodyStatements, + // header.headerVar, + // header.collectionVarNameForBailout, + // header.spec.extractMethods); + // if (extraction == null) { + // return; + // } + // // Resolve CFG-local metadata (except loopUpdateBlock). + // Block condBlock = firstBlockForTree(condNoParens); + // if (condBlock == null) { + // return; + // } + // // Find the ConditionalBlock that branches on the while condition. + // ConditionalBlock cblock = findConditionalSuccessor(condBlock); + // if (cblock == null) { + // // condition often lives in ExceptionBlocks; try walking up preds and retry + // Block peeled = peelExceptionBlocksToPred(condBlock); + // if (peeled != null) { + // cblock = findConditionalSuccessor(peeled); + // } + // } + // if (cblock == null) { + // return; + // } + // + // Block loopBodyEntryBlock = cblock.getThenSuccessor(); + // + // // Node for the extraction call tree (it.next()/q.poll()/s.pop()). + // Node elementNode = anyNodeForTree(extraction.extractionCall); + // if (elementNode == null) { + // return; + // } + // + // // 4) Record a potentially fulfilling collection loop. + // // + // // Store: + // // - collectionTree (resources / q / s) + // // - collectionElementTree (it.next() / q.poll() / s.pop()) + // // - condition tree (the while condition) + // atypeFactory.recordPotentiallyFulfillingCollectionLoop( + // enclosingMethodTree, + // header.collectionTree, + // extraction.extractionCall, + // condNoParens, + // loopBodyEntryBlock, + // cblock, + // elementNode); + // } + // + // /** + // * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a + // * lambda expression. + // * + // *

The per-method loop-state refactor records only loops that are part of the enclosing + // method + // * analysis. Lambda-local loop support can be added separately if needed. + // * + // * @return the enclosing method for the current loop, or {@code null} if it is inside a lambda + // */ + // private @Nullable MethodTree getEnclosingMethodForCollectionLoop() { + // Tree enclosingMethodOrLambda = TreePathUtil.enclosingMethodOrLambda(getCurrentPath()); + // if (enclosingMethodOrLambda instanceof MethodTree) { + // return (MethodTree) enclosingMethodOrLambda; + // } + // return null; + // } + // + // /** + // * Returns the statements in a loop body, regardless of whether the body is a block. + // * + // * @param statement the loop body statement + // * @return the loop body statements, or {@code null} if {@code statement} is {@code null} + // */ + // private @Nullable List getLoopBodyStatements( + // @Nullable StatementTree statement) { + // if (statement == null) { + // return null; + // } + // return statement instanceof BlockTree + // ? ((BlockTree) statement).getStatements() + // : Collections.singletonList(statement); + // } + // + // /** + // * Returns the first CFG block associated with the given tree. + // * + // * @param tree a tree + // * @return the first CFG block associated with {@code tree}, or {@code null} if none is known + // */ + // private @Nullable Block firstBlockForTree(Tree tree) { + // Set nodes = atypeFactory.getNodesForTree(tree); + // if (nodes == null || nodes.isEmpty()) { + // return null; + // } + // for (Node n : nodes) { + // Block block = n.getBlock(); + // if (block != null) { + // return block; + // } + // } + // return null; + // } + // + // /** + // * Returns an arbitrary CFG node associated with the given tree. + // * + // * @param tree a tree + // * @return a CFG node associated with {@code tree}, or {@code null} if none is known + // */ + // private @Nullable Node anyNodeForTree(Tree tree) { + // Set nodes = atypeFactory.getNodesForTree(tree); + // if (nodes == null || nodes.isEmpty()) { + // return null; + // } + // return nodes.iterator().next(); + // } + // + // /** + // * Returns the conditional successor reached from the given block, if one is immediately + // visible. + // * + // * @param block a CFG block + // * @return the conditional successor of {@code block}, or {@code null} if none is found + // */ + // private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { + // for (Block succ : block.getSuccessors()) { + // if (succ instanceof ConditionalBlock) { + // return (ConditionalBlock) succ; + // } + // } + // if (block instanceof SingleSuccessorBlock) { + // Block succ = ((SingleSuccessorBlock) block).getSuccessor(); + // if (succ instanceof ConditionalBlock) { + // return (ConditionalBlock) succ; + // } + // } + // return null; + // } + // + // /** + // * Walks backward through exception blocks to recover the predecessor block that leads to the + // * actual loop conditional. + // * + // *

This is needed because loop conditions such as {@code iterator.hasNext()} may be + // represented + // * by exception blocks before reaching the conditional branch. + // * + // * @param block a CFG block + // * @return a predecessor block to retry from, or {@code null} if no such block is found + // */ + // private @Nullable Block peelExceptionBlocksToPred(Block block) { + // Block cur = block; + // Set visitedBlocks = new HashSet<>(); + // while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { + // Set preds = cur.getPredecessors(); + // if (preds.size() != 1) { + // break; + // } + // Block p = preds.iterator().next(); + // if (p == null) { + // break; + // } + // cur = p; + // } + // return cur; + // } + // + // /** + // * Matches supported while-loop header forms and returns the recovered loop facts. + // * + // *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code + // * while (c.size() > 0)}, and {@code while (0 < c.size())}. + // * + // * @param cond the while-loop condition with parentheses removed + // * @return the recovered header facts, or {@code null} if the header is unsupported + // */ + // private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { + // // Case A: while (it.hasNext()) + // if (cond instanceof MethodInvocationTree) { + // MethodInvocationTree mit = (MethodInvocationTree) cond; + // if (TreeUtils.isHasNextCall(mit)) { + // ExpressionTree recv = receiverOfInvocation(mit); + // Name itName = getNameFromExpressionTree(recv); + // if (itName == null) { + // return null; + // } + // ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); + // if (colExpr == null) { + // return null; + // } + // Name colName = getNameFromExpressionTree(colExpr); + // return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); + // } + // } + // + // // Case B1: while (!c.isEmpty()) + // if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { + // ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); + // WhileHeaderMatch m = matchNonEmptyFromExpr(inner); + // if (m != null) { + // return m; + // } + // } + // + // // Case B2: while (c.size() > 0) or while (0 < c.size()) + // if (cond instanceof BinaryTree) { + // WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); + // if (m != null) { + // return m; + // } + // } + // + // return null; + // } + // + // /** + // * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. + // * + // * @param inner the expression under the logical complement + // * @return the recovered header facts, or {@code null} if the expression does not match + // */ + // private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { + // if (!(inner instanceof MethodInvocationTree)) { + // return null; + // } + // MethodInvocationTree mit = (MethodInvocationTree) inner; + // if (!isIsEmptyCall(mit)) { + // return null; + // } + // ExpressionTree recv = receiverOfInvocation(mit); + // if (recv == null) { + // return null; + // } + // Name varName = getNameFromExpressionTree(recv); + // if (varName == null) { + // return null; + // } + // Element recvElt = TreeUtils.elementFromTree(recv); + // if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + // return null; + // } + // ExpressionTree colTree = collectionTreeFromExpression(recv); + // if (colTree == null) { + // return null; + // } + // return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + // } + // + // /** + // * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < + // * c.size()}. + // * + // * @param condition the binary condition + // * @return the recovered header facts, or {@code null} if the expression does not match + // */ + // private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { + // Tree.Kind k = condition.getKind(); + // if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { + // return null; + // } + // + // ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); + // ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); + // + // // Normalize: accept "c.size() > 0" or "0 < c.size()" + // MethodInvocationTree sizeCall = null; + // LiteralTree zero = null; + // + // if (k == Tree.Kind.GREATER_THAN) { + // // left must be size(), right must be 0 + // if (left instanceof MethodInvocationTree && right instanceof LiteralTree) { + // sizeCall = (MethodInvocationTree) left; + // zero = (LiteralTree) right; + // } + // } else { // LESS_THAN + // // left must be 0, right must be size() + // if (left instanceof LiteralTree && right instanceof MethodInvocationTree) { + // zero = (LiteralTree) left; + // sizeCall = (MethodInvocationTree) right; + // } + // } + // + // if (sizeCall == null + // || !(zero.getValue() instanceof Integer) + // || (Integer) zero.getValue() != 0) { + // return null; + // } + // if (!TreeUtils.isSizeAccess(sizeCall)) { + // return null; + // } + // + // ExpressionTree recv = receiverOfInvocation(sizeCall); + // if (recv == null) { + // return null; + // } + // + // Name varName = getNameFromExpressionTree(recv); + // if (varName == null) { + // return null; + // } + // + // Element recvElt = TreeUtils.elementFromTree(recv); + // if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + // return null; + // } + // + // ExpressionTree colTree = collectionTreeFromExpression(recv); + // if (colTree == null) { + // return null; + // } + // + // return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + // } + // + // /** + // * Returns whether the given invocation is an {@code isEmpty()} call with no arguments. + // * + // * @param invocation a method invocation + // * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments + // */ + // private boolean isIsEmptyCall(MethodInvocationTree invocation) { + // ExpressionTree sel = invocation.getMethodSelect(); + // if (!(sel instanceof MemberSelectTree)) { + // return false; + // } + // MemberSelectTree ms = (MemberSelectTree) sel; + // return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); + // } + // + // /** + // * Returns the explicit receiver of the given invocation, if present. + // * + // * @param invocation a method invocation + // * @return the explicit receiver, or {@code null} if none exists + // */ + // private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { + // ExpressionTree sel = invocation.getMethodSelect(); + // if (sel instanceof MemberSelectTree) { + // return ((MemberSelectTree) sel).getExpression(); + // } + // return null; + // } + // + // /** + // * Recovers the collection expression from an iterator receiver in a header such as {@code + // while + // * (it.hasNext())}. + // * + // *

This only recognizes local iterator variables initialized by {@code col.iterator()}. + // * + // * @param iteratorExpr the iterator receiver expression + // * @return the collection expression, or {@code null} if it cannot be recovered + // */ + // private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( + // ExpressionTree iteratorExpr) { + // if (iteratorExpr == null) { + // return null; + // } + // + // Element itElt = TreeUtils.elementFromTree(iteratorExpr); + // if (!(itElt instanceof VariableElement)) { + // return null; + // } + // + // // Only recover from local variable declaration with initializer "col.iterator()" + // if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { + // return null; + // } + // + // Tree decl = atypeFactory.declarationFromElement(itElt); + // if (!(decl instanceof VariableTree)) { + // return null; + // } + // + // ExpressionTree init = ((VariableTree) decl).getInitializer(); + // if (!(init instanceof MethodInvocationTree)) { + // return null; + // } + // + // MethodInvocationTree initCall = (MethodInvocationTree) init; + // ExpressionTree sel = initCall.getMethodSelect(); + // if (!(sel instanceof MemberSelectTree)) { + // return null; + // } + // + // MemberSelectTree ms = (MemberSelectTree) sel; + // if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { + // return null; + // } + // + // ExpressionTree colExpr = ms.getExpression(); + // Element colElt = TreeUtils.elementFromTree(colExpr); + // if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { + // return null; + // } + // + // return collectionTreeFromExpression(colExpr); + // } + // + // /** + // * One extracted element use recovered from a while-loop body. + // * + // *

The extraction call is the expression that removes or advances to the next element, such + // as + // * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. + // */ + // private static final class BodyExtraction { + // /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ + // final MethodInvocationTree extractionCall; + // + // /** + // * Creates a body extraction summary. + // * + // * @param extractionCall extraction call found in the loop body + // */ + // BodyExtraction(MethodInvocationTree extractionCall) { + // this.extractionCall = extractionCall; + // } + // } + // + // /** + // * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code + // * null}. + // * + // *

This matcher rejects writes to the iterator/header variable and, when present, to the + // * collection variable itself, because such writes invalidate the header/body correspondence + // used + // * by later CFG verification. + // * + // * @param statements the loop body statements + // * @param headerVar the iterator or collection variable constrained by the header + // * @param collectionVarName the collection variable to protect from writes, if any + // * @param allowedExtractMethods the extraction methods allowed by the matched header + // * @return the unique extraction in the loop body, or {@code null} if the body is unsupported + // */ + // private @Nullable BodyExtraction findSingleExtractionInWhileBody( + // List statements, + // Name headerVar, + // @Nullable Name collectionVarName, + // Set allowedExtractMethods) { + // + // AtomicBoolean illegal = new AtomicBoolean(false); + // final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; + // final int[] extractionCount = new int[] {0}; + // + // TreeScanner scanner = + // new TreeScanner() { + // + // private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { + // Name assigned = getNameFromExpressionTree(lhs); + // if (assigned != null) { + // if (assigned == headerVar) illegal.set(true); + // if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); + // } + // } + // + // private void recordExtractionIfAny(ExpressionTree expr) { + // expr = TreeUtils.withoutParens(expr); + // if (!(expr instanceof MethodInvocationTree)) { + // return; + // } + // + // MethodInvocationTree mit = (MethodInvocationTree) expr; + // if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { + // return; + // } + // + // extractionCount[0]++; + // if (extractionCount[0] > 1) { + // illegal.set(true); + // return; + // } + // extraction[0] = mit; + // } + // + // @Override + // public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { + // markWriteIfTargetsHeaderOrCollection(node.getVariable()); + // return super.visitCompoundAssignment(node, p); + // } + // + // @Override + // public Void visitAssignment(AssignmentTree node, Void p) { + // markWriteIfTargetsHeaderOrCollection(node.getVariable()); + // recordExtractionIfAny(node.getExpression()); // r = it.next() + // return super.visitAssignment(node, p); + // } + // + // @Override + // public Void visitVariable(VariableTree vt, Void p) { + // ExpressionTree init = vt.getInitializer(); + // if (init != null) { + // recordExtractionIfAny(init); // T r = it.next() + // } + // return super.visitVariable(vt, p); + // } + // + // @Override + // public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + // // Direct use: it.next().close() => receiver is it.next() + // ExpressionTree sel = mit.getMethodSelect(); + // if (sel instanceof MemberSelectTree) { + // ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); + // recordExtractionIfAny(recv); + // } + // return super.visitMethodInvocation(mit, p); + // } + // }; + // + // for (StatementTree st : statements) { + // scanner.scan(st, null); + // if (illegal.get()) break; + // } + // + // if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { + // return null; + // } + // return new BodyExtraction(extraction[0]); + // } + // + // /** + // * Returns whether the given invocation is an allowed extraction call on the matched header + // * variable. + // * + // * @param invocation a method invocation + // * @param headerVar the iterator or collection variable constrained by the header + // * @param allowedExtractMethods extraction methods permitted by the matched header form + // * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} + // */ + // private boolean isExtractionCallOnHeaderVar( + // MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { + // ExpressionTree sel = invocation.getMethodSelect(); + // if (!(sel instanceof MemberSelectTree)) { + // return false; + // } + // MemberSelectTree ms = (MemberSelectTree) sel; + // String methodName = ms.getIdentifier().toString(); + // if (!allowedExtractMethods.contains(methodName)) { + // return false; + // } + // if (!invocation.getArguments().isEmpty()) { + // return false; + // } + // Name recv = getNameFromExpressionTree(ms.getExpression()); + // return recv != null && recv == headerVar; + // } + // + // /** + // * Marks the for-loop if it potentially fulfills collection obligations of a collection. + // * + // * @param tree a `for` loop with exactly one loop variable + // */ + // private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { + // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); + // if (enclosingMethodTree == null) { + // return; + // } + // + // List loopBodyStatements = + // getLoopBodyStatements(tree.getStatement()); + // if (loopBodyStatements == null) { + // return; + // } + // StatementTree init = tree.getInitializer().get(0); + // ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); + // ExpressionStatementTree update = tree.getUpdate().get(0); + // if (!(condition instanceof BinaryTree)) { + // return; + // } + // Name identifierInHeader = + // nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); + // Name iterator = getNameFromStatementTree(init); + // if (identifierInHeader == null || iterator == null) { + // return; + // } + // ExpressionTree collectionElementTree = + // getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); + // if (collectionElementTree != null) { + // // Pattern match succeeded, now mark the loop in the respective datastructures. + // + // Block loopConditionBlock = null; + // for (Node node : atypeFactory.getNodesForTree(condition)) { + // Block blockOfNode = node.getBlock(); + // if (blockOfNode != null) { + // loopConditionBlock = blockOfNode; + // break; + // } + // } + // + // Block loopUpdateBlock = null; + // for (Node node : atypeFactory.getNodesForTree(update.getExpression())) { + // Block blockOfNode = node.getBlock(); + // if (blockOfNode != null) { + // loopUpdateBlock = blockOfNode; + // break; + // } + // } + // + // Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); + // Node nodeForCollectionElt = null; + // if (collectionEltNodes != null) { + // nodeForCollectionElt = collectionEltNodes.iterator().next(); + // } + // if (loopUpdateBlock == null || loopConditionBlock == null) { + // return; + // } + // // Record the loop in the RLCCalledMethods ATF's per-method loop state so that it can + // // analyze it later. + // // MustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop will + // then + // // add verified fulfilling loops to the collection-ownership ATF. + // Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); + // Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); + // atypeFactory.recordResolvedPotentiallyFulfillingCollectionLoop( + // enclosingMethodTree, + // collectionTreeFromExpression(collectionElementTree), + // collectionElementTree, + // tree.getCondition(), + // loopBodyEntryBlock, + // loopUpdateBlock, + // (ConditionalBlock) conditionalBlock, + // nodeForCollectionElt); + // } + // } + // + // /** + // * Conservatively decides whether a loop iterates over all elements of some collection, using + // the + // * following rules: + // * + // *

    + // *
  • only one loop variable + // *
  • initialization must be of the form i = 0 + // *
  • condition must be of the form (i < col.size()) + // *
  • update must be prefix or postfix {@code ++} + // *
+ // * + // * Returns: + // * + // *
    + // *
  • null, if any of the above rules is violated + // *
  • the name of the collection if the loop condition is of the form (i < col.size()) + // *
+ // * + // * @param init the initializer of the loop + // * @param condition the loop condition + // * @param update the loop update + // * @return the name of the collection that the loop iterates over all elements of, or null + // */ + // protected Name nameOfCollectionThatAllElementsAreCalledOn( + // StatementTree init, BinaryTree condition, ExpressionStatementTree update) { + // Tree.Kind updateKind = update.getExpression().getKind(); + // if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { + // UnaryTree inc = (UnaryTree) update.getExpression(); + // + // // Verify update is of form i++ or ++i and init is variable initializer. + // if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) + // return null; + // VariableTree initVar = (VariableTree) init; + // + // // Verify that intializer is i=0. + // if (!(initVar.getInitializer() instanceof LiteralTree) + // || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { + // return null; + // } + // + // // Verify that condition is of the form: i < something. + // if (!(condition.getLeftOperand() instanceof IdentifierTree)) { + // return null; + // } + // + // // Verify that i=0, i statements, Name identifierInHeader, Name iterator) { + // AtomicBoolean blockIsIllegal = new AtomicBoolean(false); + // final ExpressionTree[] collectionElementTree = {null}; + // + // TreeScanner scanner = + // new TreeScanner() { + // @Override + // public Void visitUnary(UnaryTree tree, Void p) { + // switch (tree.getKind()) { + // case PREFIX_DECREMENT: + // case POSTFIX_DECREMENT: + // case PREFIX_INCREMENT: + // case POSTFIX_INCREMENT: + // if (getNameFromExpressionTree(tree.getExpression()) == iterator) { + // blockIsIllegal.set(true); + // } + // break; + // default: + // break; + // } + // return super.visitUnary(tree, p); + // } + // + // @Override + // public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + // if (getNameFromExpressionTree(tree.getVariable()) == iterator) { + // blockIsIllegal.set(true); + // } + // return super.visitCompoundAssignment(tree, p); + // } + // + // @Override + // public Void visitAssignment(AssignmentTree tree, Void p) { + // Name assignedVariable = getNameFromExpressionTree(tree.getVariable()); + // if (assignedVariable == iterator || assignedVariable == identifierInHeader) { + // blockIsIllegal.set(true); + // } + // + // return super.visitAssignment(tree, p); + // } + // + // // check whether corresponds to collection.get(i) + // @Override + // public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { + // if (isIthCollectionElement(mit, iterator) + // && identifierInHeader == getNameFromExpressionTree(mit) + // && identifierInHeader != null) { + // collectionElementTree[0] = mit; + // } + // return super.visitMethodInvocation(mit, p); + // } + // }; + // + // for (StatementTree stmt : statements) { + // scanner.scan(stmt, null); + // } + // if (!blockIsIllegal.get() && collectionElementTree[0] != null) { + // return collectionElementTree[0]; + // } + // return null; + // } + // + // /** + // * Returns the simple name of the identifier referenced by the given expression, or {@code + // null} + // * if the expression does not reference an identifier. + // * + // * @param expr an expression + // * @return the name of the referenced identifier, or {@code null} if none + // */ + // protected Name getNameFromExpressionTree(ExpressionTree expr) { + // if (expr == null) { + // return null; + // } + // switch (expr.getKind()) { + // case IDENTIFIER: + // return ((IdentifierTree) expr).getName(); + // case MEMBER_SELECT: + // MemberSelectTree mst = (MemberSelectTree) expr; + // Element elt = TreeUtils.elementFromUse(mst); + // if (elt.getKind() == ElementKind.FIELD) { + // // this.files ==> "files" (NOT "this") + // return mst.getIdentifier(); + // } else if (elt.getKind() == ElementKind.METHOD) { + // // resources.size ==> "resources" + // return getNameFromExpressionTree(mst.getExpression()); + // } else { + // return null; + // } + // case METHOD_INVOCATION: + // return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); + // default: + // return null; + // } + // } + // + // /** + // * Returns the simple name of the identifier declared or referenced by the given statement, or + // * {@code null} if the statement does not declare or reference an identifier. + // * + // * @param expr the {@code StatementTree} + // * @return the name of the identifier declared or referenced by the statement, or {@code null} + // if + // * none + // */ + // protected Name getNameFromStatementTree(StatementTree expr) { + // if (expr == null) { + // return null; + // } + // switch (expr.getKind()) { + // case VARIABLE: + // return ((VariableTree) expr).getName(); + // case EXPRESSION_STATEMENT: + // return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); + // default: + // return null; + // } + // } + // + // /** + // * Returns the ExpressionTree of the collection in the given expression. + // * + // * @param expr ExpressionTree + // * @return the expression evaluates to or null if it doesn't + // */ + // protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { + // switch (expr.getKind()) { + // case IDENTIFIER: + // return expr; + // case MEMBER_SELECT: + // MemberSelectTree mst = (MemberSelectTree) expr; + // Element elt = TreeUtils.elementFromUse(mst); + // if (elt.getKind() == ElementKind.METHOD) { + // return ((MemberSelectTree) expr).getExpression(); + // } else if (elt.getKind() == ElementKind.FIELD) { + // return expr; + // } else { + // return null; + // } + // case METHOD_INVOCATION: + // return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); + // default: + // return null; + // } + // } + // + // /** + // * Returns true if the given tree is of the form collection.get(i), where i is the given index + // * name. + // * + // * @param tree the tree to check + // * @param index the index variable name + // * @return true if the given tree is of the form collection.get(index) + // */ + // private boolean isIthCollectionElement(Tree tree, Name index) { + // if (tree == null || index == null) { + // return false; + // } + // if (tree instanceof MethodInvocationTree + // && index == getNameFromExpressionTree(TreeUtils.getIdxForGetCall(tree))) { + // MethodInvocationTree mit = (MethodInvocationTree) tree; + // ExpressionTree methodSelect = mit.getMethodSelect(); + // if (methodSelect instanceof MemberSelectTree) { + // MemberSelectTree mst = (MemberSelectTree) methodSelect; + // Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); + // return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + // } + // } + // return false; + // } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 34fbc5010f9f..4e01e509c1a4 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -43,7 +43,8 @@ import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory; import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.collectionownership.CollectionOwnershipStore; -import org.checkerframework.checker.collectionownership.DisposalLoopCoordinator.DisposalLoop; +import org.checkerframework.checker.collectionownership.CollectionOwnershipUtils; +import org.checkerframework.checker.collectionownership.DisposalLoop; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -54,7 +55,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnalysis; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; -import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsVisitor; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; @@ -801,7 +801,7 @@ private boolean canReachRegularExit(Block block) { */ private Set computeBlocksThatCanReachRegularExit(ControlFlowGraph cfg) { Block entry = cfg.getEntryBlock(); - Set reachable = RLCCalledMethodsAnnotatedTypeFactory.reachableFrom(entry); + Set reachable = CollectionOwnershipUtils.reachableFrom(entry); Block normalExit = cfg.getRegularExitBlock(); Set exitBlocks = new HashSet<>(); if (reachable.contains(normalExit)) { @@ -3477,12 +3477,11 @@ public static String collectionToString(Collection bwos) { * @return the called-methods for the loop on the iterated element, or {@code null} if there's no * definite called-methods on the iterated element of the loop */ - public Set analyzeDisposalLoop( - ControlFlowGraph cfg, ResolvedPotentiallyFulfillingCollectionLoop disposalLoop) { + public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoop disposalLoop) { // ensure checked loop is initialized in a valid way Objects.requireNonNull( - disposalLoop.collectionElementTree, + disposalLoop.iteratedElementTree, "CollectionElementAccess tree provided to analyze loop body of an" + " CFG-resolved potentially fulfilling collection loop is null."); Objects.requireNonNull( @@ -3496,7 +3495,7 @@ public Set analyzeDisposalLoop( Block loopBodyEntryBlock = disposalLoop.loopBodyEntryBlock; Block loopUpdateBlock = disposalLoop.loopUpdateBlock; - Tree collectionElement = disposalLoop.collectionElementTree; + Tree collectionElement = disposalLoop.iteratedElementTree; boolean emptyLoopBody = loopBodyEntryBlock.equals(loopUpdateBlock); if (emptyLoopBody) { @@ -3674,7 +3673,7 @@ private Set computeLoopRegion(Block entry, Block update) { * aliases. * * @param lastLoopBodyBlock last block of loop body - * @param resolvedPotentiallyFulfillingLoop loop wrapper of the loop to analyze + * @param disposalLoop loop wrapper of the loop to analyze * @param obligations the set of tracked obligations * @param loopUpdateBlock block that updates the loop * @return the union of methods in the CalledMethods type of the collection element and all its @@ -3682,7 +3681,7 @@ private Set computeLoopRegion(Block entry, Block update) { */ private Set analyzeTypeOfCollectionElement( Block lastLoopBodyBlock, - ResolvedPotentiallyFulfillingCollectionLoop resolvedPotentiallyFulfillingLoop, + DisposalLoop disposalLoop, Set obligations, Block loopUpdateBlock) { AccumulationStore store = null; @@ -3698,7 +3697,7 @@ private Set analyzeTypeOfCollectionElement( store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); } Obligation collectionElementObligation = - getObligationForVar(obligations, resolvedPotentiallyFulfillingLoop.collectionElementTree); + getObligationForVar(obligations, disposalLoop.iteratedElementTree); if (collectionElementObligation == null) { // the loop did something weird. Might have reassigned the collection element. // The sound thing to do is return an empty list. @@ -3714,8 +3713,7 @@ private Set analyzeTypeOfCollectionElement( // add the called methods of the ICE IteratedCollectionElement ice = store.getIteratedCollectionElement( - resolvedPotentiallyFulfillingLoop.collectionElementNode, - resolvedPotentiallyFulfillingLoop.collectionElementTree); + disposalLoop.iteratedElementNode, disposalLoop.iteratedElementTree); if (ice != null) { AccumulationValue cmValOfIce = store.getValue(ice); List calledMethods = getCalledMethods(cmValOfIce); diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 7f448891ee46..0f51703bf185 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -3,8 +3,6 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.EnhancedForLoopTree; -import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; @@ -12,18 +10,9 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import java.lang.annotation.Annotation; -import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Queue; import java.util.Set; import javax.lang.model.element.AnnotationMirror; @@ -47,7 +36,6 @@ import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; import org.checkerframework.checker.resourceleak.ResourceLeakChecker; import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; @@ -58,9 +46,6 @@ import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; -import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; @@ -71,7 +56,6 @@ import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.util.Contract; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; @@ -131,345 +115,353 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated */ private final BiMap tempVarToTree = HashBiMap.create(); - /** - * Per-method loop state for collection-obligation loops that have been matched syntactically by - * the MustCall visitor. - */ - private static final class MethodCollectionLoopState { - - /** Creates per-method loop state for syntactically matched collection-obligation loops. */ - private MethodCollectionLoopState() {} - - /** Enhanced-for-loops that have been matched syntactically but still need CFG resolution. */ - final Set potentiallyFulfillingEnhancedForLoops = new LinkedHashSet<>(); - - /** - * Collection-obligation loops that have been matched syntactically but still need CFG-local - * resolution before the consistency analyzer can verify them. - */ - final Set potentiallyFulfillingCollectionLoops = - new LinkedHashSet<>(); - - /** - * Potentially fulfilling collection loops that have all CFG information required by the - * consistency analyzer. - */ - final Set - resolvedPotentiallyFulfillingCollectionLoops = new LinkedHashSet<>(); - - /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ - private @Nullable WhileLoopResolutionCache whileLoopCache; - - /** - * Returns CFG facts for resolving potentially fulfilling while loops in this method, creating - * them lazily if needed. - * - * @param cfg the enclosing method CFG - * @return the CFG facts for resolving potentially fulfilling while loops in this method - */ - private WhileLoopResolutionCache getOrCreateWhileLoopCache(ControlFlowGraph cfg) { - if (whileLoopCache == null) { - whileLoopCache = new WhileLoopResolutionCache(cfg); - } - return whileLoopCache; - } - } - - /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ - private static final class WhileLoopResolutionCache { - - /** A back edge in the CFG. */ - private static final class BlockEdge { - /** Source block of the back edge. */ - final Block sourceBlock; - - /** Target block of the back edge. */ - final Block targetBlock; - - /** - * Creates a CFG back edge description. - * - * @param sourceBlock source block of the back edge - * @param targetBlock target block of the back edge - */ - BlockEdge(Block sourceBlock, Block targetBlock) { - this.sourceBlock = sourceBlock; - this.targetBlock = targetBlock; - } - } - - /** Reachable CFG blocks in the current method. */ - private final Set reachableBlocks; - - /** Back edges among {@link #reachableBlocks}. */ - private final List backEdges; - - /** Natural loops for back edges, computed lazily. */ - private final IdentityHashMap> naturalLoopsByBackEdge = - new IdentityHashMap<>(); - - /** - * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. - * - * @param cfg the enclosing method CFG - */ - private WhileLoopResolutionCache(ControlFlowGraph cfg) { - Block entryBlock = cfg.getEntryBlock(); - this.reachableBlocks = reachableFrom(entryBlock); - Map> dominators = computeDominators(entryBlock, reachableBlocks); - this.backEdges = findBackEdges(reachableBlocks, dominators); - } - - /** - * Returns the back edges among the reachable blocks in the current CFG. - * - * @return the CFG back edges - */ - private List getBackEdges() { - return backEdges; - } - - /** - * Returns the natural loop induced by the given back edge, computing it lazily if needed. - * - * @param backEdge the back edge - * @return the natural loop induced by the given back edge - */ - private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { - return naturalLoopsByBackEdge.computeIfAbsent( - backEdge, - ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); - } - - /** - * Returns blocks reachable from {@code entryBlock}. - * - * @param entryBlock the CFG entry block - * @return the reachable blocks - */ - private static Set reachableFrom(Block entryBlock) { - Set seen = new HashSet<>(); - ArrayDeque queue = new ArrayDeque<>(); - queue.add(entryBlock); - seen.add(entryBlock); - - while (!queue.isEmpty()) { - Block block = queue.remove(); - for (Block successor : block.getSuccessors()) { - if (successor != null && seen.add(successor)) { - queue.add(successor); - } - } - } - return seen; - } - - /** - * Computes dominators for the reachable blocks in the current CFG. - * - * @param entryBlock the CFG entry block - * @param reachableBlocks reachable blocks in the CFG - * @return dominators for each reachable block - */ - private static Map> computeDominators( - Block entryBlock, Set reachableBlocks) { - Map> dominators = new HashMap<>(); - - for (Block block : reachableBlocks) { - if (block.equals(entryBlock)) { - dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); - } else { - dominators.put(block, new HashSet<>(reachableBlocks)); // TOP - } - } - - boolean changed; - do { - changed = false; - for (Block block : reachableBlocks) { - if (block.equals(entryBlock)) { - continue; - } - - Set newDominators = null; - for (Block predecessor : block.getPredecessors()) { - if (predecessor == null || !reachableBlocks.contains(predecessor)) { - continue; - } - Set predecessorDominators = dominators.get(predecessor); - if (predecessorDominators == null) { - continue; - } - if (newDominators == null) { - newDominators = new HashSet<>(predecessorDominators); - } else { - newDominators.retainAll(predecessorDominators); - } - } - - if (newDominators == null) { - newDominators = new HashSet<>(); - } - newDominators.add(block); - - if (!newDominators.equals(dominators.get(block))) { - dominators.put(block, newDominators); - changed = true; - } - } - } while (changed); - - return dominators; - } - - /** - * Returns the back edges among the reachable blocks in the current CFG. - * - * @param reachableBlocks reachable blocks in the CFG - * @param dominators dominators for each reachable block - * @return the CFG back edges - */ - private static List findBackEdges( - Set reachableBlocks, Map> dominators) { - List backEdges = new ArrayList<>(); - for (Block sourceBlock : reachableBlocks) { - for (Block targetBlock : sourceBlock.getSuccessors()) { - if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { - continue; - } - Set sourceDominators = dominators.get(sourceBlock); - if (sourceDominators != null && sourceDominators.contains(targetBlock)) { - // targetBlock dominates sourceBlock, so sourceBlock -> targetBlock is a back edge. - backEdges.add(new BlockEdge(sourceBlock, targetBlock)); - } - } - } - return backEdges; - } - - /** - * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. - * - * @param sourceBlock the source of the back edge - * @param targetBlock the target of the back edge - * @param reachableBlocks reachable blocks in the CFG - * @return the natural loop induced by the back edge - */ - private static Set naturalLoop( - Block sourceBlock, Block targetBlock, Set reachableBlocks) { - Set loopBlocks = new HashSet<>(); - ArrayDeque stack = new ArrayDeque<>(); - - loopBlocks.add(targetBlock); - if (loopBlocks.add(sourceBlock)) { - stack.push(sourceBlock); - } - - while (!stack.isEmpty()) { - Block block = stack.pop(); - for (Block predecessor : block.getPredecessors()) { - if (predecessor == null || !reachableBlocks.contains(predecessor)) { - continue; - } - if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { - stack.push(predecessor); - } - } - } - return loopBlocks; - } - } - - /** Per-method collection-loop state accumulated during MustCall visitor matching. */ - private final IdentityHashMap - collectionLoopStateByEnclosingMethod = new IdentityHashMap<>(); - - /** - * Returns the loop state for the given method, creating it if needed. - * - * @param enclosingMethodTree the enclosing method - * @return the loop state for the given method - */ - private MethodCollectionLoopState getOrCreateMethodCollectionLoopState( - MethodTree enclosingMethodTree) { - return collectionLoopStateByEnclosingMethod.computeIfAbsent( - enclosingMethodTree, ignored -> new MethodCollectionLoopState()); - } - - /** - * Returns the loop state for the given underlying AST, or {@code null} if there is none. - * - * @param underlyingAST the current underlying AST - * @return the loop state for the given underlying AST, or {@code null} - */ - private @Nullable MethodCollectionLoopState getMethodCollectionLoopState( - UnderlyingAST underlyingAST) { - MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); - if (enclosingMethodTree == null) { - return null; - } - return collectionLoopStateByEnclosingMethod.get(enclosingMethodTree); - } - - /** - * Returns the potentially fulfilling collection loops for the method represented by the given - * underlying AST. - * - * @param underlyingAST the current underlying AST - * @return the potentially fulfilling collection loops for the method represented by the given - * underlying AST - */ - Set getPotentiallyFulfillingCollectionLoops( - UnderlyingAST underlyingAST) { - MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); - if (loopState == null) { - return Collections.emptySet(); - } - return loopState.potentiallyFulfillingCollectionLoops; - } - - /** - * Returns the resolved potentially fulfilling collection loops for the method represented by the - * given underlying AST. - * - * @param underlyingAST the current underlying AST - * @return the resolved potentially fulfilling collection loops for the method represented by the - * given underlying AST - */ - Set getResolvedPotentiallyFulfillingCollectionLoops( - UnderlyingAST underlyingAST) { - MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); - if (loopState == null) { - return Collections.emptySet(); - } - return loopState.resolvedPotentiallyFulfillingCollectionLoops; - } - - /** - * Removes the loop state associated with the given underlying AST. - * - * @param underlyingAST the current underlying AST - */ - private void removeMethodCollectionLoopState(UnderlyingAST underlyingAST) { - MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); - if (enclosingMethodTree != null) { - collectionLoopStateByEnclosingMethod.remove(enclosingMethodTree); - } - } - - /** - * Returns the enclosing method for the given underlying AST, or {@code null} if the underlying - * AST is not a method. - * - * @param underlyingAST the current underlying AST - * @return the enclosing method for the given underlying AST, or {@code null} - */ - private @Nullable MethodTree getEnclosingMethodTree(UnderlyingAST underlyingAST) { - if (underlyingAST.getKind() != UnderlyingAST.Kind.METHOD) { - return null; - } - return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); - } + // /** + // * Per-method loop state for collection-obligation loops that have been matched syntactically + // by + // * the MustCall visitor. + // */ + // private static final class MethodCollectionLoopState { + // + // /** Creates per-method loop state for syntactically matched collection-obligation loops. */ + // private MethodCollectionLoopState() {} + // + // /** Enhanced-for-loops that have been matched syntactically but still need CFG resolution. + // */ + // final Set potentiallyFulfillingEnhancedForLoops = new + // LinkedHashSet<>(); + // + // /** + // * Collection-obligation loops that have been matched syntactically but still need CFG-local + // * resolution before the consistency analyzer can verify them. + // */ + // final Set potentiallyFulfillingCollectionLoops = + // new LinkedHashSet<>(); + // + // /** + // * Potentially fulfilling collection loops that have all CFG information required by the + // * consistency analyzer. + // */ + // final Set + // resolvedPotentiallyFulfillingCollectionLoops = new LinkedHashSet<>(); + // + // /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ + // private @Nullable WhileLoopResolutionCache whileLoopCache; + // + // /** + // * Returns CFG facts for resolving potentially fulfilling while loops in this method, + // creating + // * them lazily if needed. + // * + // * @param cfg the enclosing method CFG + // * @return the CFG facts for resolving potentially fulfilling while loops in this method + // */ + // private WhileLoopResolutionCache getOrCreateWhileLoopCache(ControlFlowGraph cfg) { + // if (whileLoopCache == null) { + // whileLoopCache = new WhileLoopResolutionCache(cfg); + // } + // return whileLoopCache; + // } + // } + // + // /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ + // private static final class WhileLoopResolutionCache { + // + // /** A back edge in the CFG. */ + // private static final class BlockEdge { + // /** Source block of the back edge. */ + // final Block sourceBlock; + // + // /** Target block of the back edge. */ + // final Block targetBlock; + // + // /** + // * Creates a CFG back edge description. + // * + // * @param sourceBlock source block of the back edge + // * @param targetBlock target block of the back edge + // */ + // BlockEdge(Block sourceBlock, Block targetBlock) { + // this.sourceBlock = sourceBlock; + // this.targetBlock = targetBlock; + // } + // } + // + // /** Reachable CFG blocks in the current method. */ + // private final Set reachableBlocks; + // + // /** Back edges among {@link #reachableBlocks}. */ + // private final List backEdges; + // + // /** Natural loops for back edges, computed lazily. */ + // private final IdentityHashMap> naturalLoopsByBackEdge = + // new IdentityHashMap<>(); + // + // /** + // * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. + // * + // * @param cfg the enclosing method CFG + // */ + // private WhileLoopResolutionCache(ControlFlowGraph cfg) { + // Block entryBlock = cfg.getEntryBlock(); + // this.reachableBlocks = reachableFrom(entryBlock); + // Map> dominators = computeDominators(entryBlock, reachableBlocks); + // this.backEdges = findBackEdges(reachableBlocks, dominators); + // } + // + // /** + // * Returns the back edges among the reachable blocks in the current CFG. + // * + // * @return the CFG back edges + // */ + // private List getBackEdges() { + // return backEdges; + // } + // + // /** + // * Returns the natural loop induced by the given back edge, computing it lazily if needed. + // * + // * @param backEdge the back edge + // * @return the natural loop induced by the given back edge + // */ + // private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { + // return naturalLoopsByBackEdge.computeIfAbsent( + // backEdge, + // ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); + // } + // + // /** + // * Returns blocks reachable from {@code entryBlock}. + // * + // * @param entryBlock the CFG entry block + // * @return the reachable blocks + // */ + // private static Set reachableFrom(Block entryBlock) { + // Set seen = new HashSet<>(); + // ArrayDeque queue = new ArrayDeque<>(); + // queue.add(entryBlock); + // seen.add(entryBlock); + // + // while (!queue.isEmpty()) { + // Block block = queue.remove(); + // for (Block successor : block.getSuccessors()) { + // if (successor != null && seen.add(successor)) { + // queue.add(successor); + // } + // } + // } + // return seen; + // } + + // /** + // * Computes dominators for the reachable blocks in the current CFG. + // * + // * @param entryBlock the CFG entry block + // * @param reachableBlocks reachable blocks in the CFG + // * @return dominators for each reachable block + // */ + // private static Map> computeDominators( + // Block entryBlock, Set reachableBlocks) { + // Map> dominators = new HashMap<>(); + // + // for (Block block : reachableBlocks) { + // if (block.equals(entryBlock)) { + // dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); + // } else { + // dominators.put(block, new HashSet<>(reachableBlocks)); // TOP + // } + // } + // + // boolean changed; + // do { + // changed = false; + // for (Block block : reachableBlocks) { + // if (block.equals(entryBlock)) { + // continue; + // } + // + // Set newDominators = null; + // for (Block predecessor : block.getPredecessors()) { + // if (predecessor == null || !reachableBlocks.contains(predecessor)) { + // continue; + // } + // Set predecessorDominators = dominators.get(predecessor); + // if (predecessorDominators == null) { + // continue; + // } + // if (newDominators == null) { + // newDominators = new HashSet<>(predecessorDominators); + // } else { + // newDominators.retainAll(predecessorDominators); + // } + // } + // + // if (newDominators == null) { + // newDominators = new HashSet<>(); + // } + // newDominators.add(block); + // + // if (!newDominators.equals(dominators.get(block))) { + // dominators.put(block, newDominators); + // changed = true; + // } + // } + // } while (changed); + // + // return dominators; + // } + + // /** + // * Returns the back edges among the reachable blocks in the current CFG. + // * + // * @param reachableBlocks reachable blocks in the CFG + // * @param dominators dominators for each reachable block + // * @return the CFG back edges + // */ + // private static List findBackEdges( + // Set reachableBlocks, Map> dominators) { + // List backEdges = new ArrayList<>(); + // for (Block sourceBlock : reachableBlocks) { + // for (Block targetBlock : sourceBlock.getSuccessors()) { + // if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { + // continue; + // } + // Set sourceDominators = dominators.get(sourceBlock); + // if (sourceDominators != null && sourceDominators.contains(targetBlock)) { + // // targetBlock dominates sourceBlock, so sourceBlock -> targetBlock is a back edge. + // backEdges.add(new BlockEdge(sourceBlock, targetBlock)); + // } + // } + // } + // return backEdges; + // } + + // /** + // * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. + // * + // * @param sourceBlock the source of the back edge + // * @param targetBlock the target of the back edge + // * @param reachableBlocks reachable blocks in the CFG + // * @return the natural loop induced by the back edge + // */ + // private static Set naturalLoop( + // Block sourceBlock, Block targetBlock, Set reachableBlocks) { + // Set loopBlocks = new HashSet<>(); + // ArrayDeque stack = new ArrayDeque<>(); + // + // loopBlocks.add(targetBlock); + // if (loopBlocks.add(sourceBlock)) { + // stack.push(sourceBlock); + // } + // + // while (!stack.isEmpty()) { + // Block block = stack.pop(); + // for (Block predecessor : block.getPredecessors()) { + // if (predecessor == null || !reachableBlocks.contains(predecessor)) { + // continue; + // } + // if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { + // stack.push(predecessor); + // } + // } + // } + // return loopBlocks; + // } + // } + + // /** Per-method collection-loop state accumulated during MustCall visitor matching. */ + // private final IdentityHashMap + // collectionLoopStateByEnclosingMethod = new IdentityHashMap<>(); + + // /** + // * Returns the loop state for the given method, creating it if needed. + // * + // * @param enclosingMethodTree the enclosing method + // * @return the loop state for the given method + // */ + // private MethodCollectionLoopState getOrCreateMethodCollectionLoopState( + // MethodTree enclosingMethodTree) { + // return collectionLoopStateByEnclosingMethod.computeIfAbsent( + // enclosingMethodTree, ignored -> new MethodCollectionLoopState()); + // } + + // /** + // * Returns the loop state for the given underlying AST, or {@code null} if there is none. + // * + // * @param underlyingAST the current underlying AST + // * @return the loop state for the given underlying AST, or {@code null} + // */ + // private @Nullable MethodCollectionLoopState getMethodCollectionLoopState( + // UnderlyingAST underlyingAST) { + // MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); + // if (enclosingMethodTree == null) { + // return null; + // } + // return collectionLoopStateByEnclosingMethod.get(enclosingMethodTree); + // } + + // /** + // * Returns the potentially fulfilling collection loops for the method represented by the given + // * underlying AST. + // * + // * @param underlyingAST the current underlying AST + // * @return the potentially fulfilling collection loops for the method represented by the given + // * underlying AST + // */ + // Set getPotentiallyFulfillingCollectionLoops( + // UnderlyingAST underlyingAST) { + // MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); + // if (loopState == null) { + // return Collections.emptySet(); + // } + // return loopState.potentiallyFulfillingCollectionLoops; + // } + + // /** + // * Returns the resolved potentially fulfilling collection loops for the method represented by + // the + // * given underlying AST. + // * + // * @param underlyingAST the current underlying AST + // * @return the resolved potentially fulfilling collection loops for the method represented by + // the + // * given underlying AST + // */ + // Set + // getResolvedPotentiallyFulfillingCollectionLoops( + // UnderlyingAST underlyingAST) { + // MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); + // if (loopState == null) { + // return Collections.emptySet(); + // } + // return loopState.resolvedPotentiallyFulfillingCollectionLoops; + // } + + // /** + // * Removes the loop state associated with the given underlying AST. + // * + // * @param underlyingAST the current underlying AST + // */ + // private void removeMethodCollectionLoopState(UnderlyingAST underlyingAST) { + // MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); + // if (enclosingMethodTree != null) { + // collectionLoopStateByEnclosingMethod.remove(enclosingMethodTree); + // } + // } + + // /** + // * Returns the enclosing method for the given underlying AST, or {@code null} if the + // underlying + // * AST is not a method. + // * + // * @param underlyingAST the current underlying AST + // * @return the enclosing method for the given underlying AST, or {@code null} + // */ + // private @Nullable MethodTree getEnclosingMethodTree(UnderlyingAST underlyingAST) { + // if (underlyingAST.getKind() != UnderlyingAST.Kind.METHOD) { + // return null; + // } + // return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); + // } /** * Creates a new RLCCalledMethodsAnnotatedTypeFactory. @@ -545,83 +537,91 @@ protected ControlFlowGraph analyze( capturedStore); } - /** - * Records a while-like collection loop that matched syntactically and still needs CFG resolution. - * - * @param enclosingMethodTree enclosing method that contains the loop - * @param collectionTree collection expression whose elements may be discharged - * @param collectionElementTree tree for the element extracted from the collection - * @param conditionTree loop condition tree - * @param loopBodyEntryBlock first block of the loop body - * @param loopConditionalBlock conditional block that controls the loop - * @param collectionElementNode CFG node for the extracted collection element - */ - public void recordPotentiallyFulfillingCollectionLoop( - MethodTree enclosingMethodTree, - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree conditionTree, - Block loopBodyEntryBlock, - ConditionalBlock loopConditionalBlock, - Node collectionElementNode) { - getOrCreateMethodCollectionLoopState(enclosingMethodTree) - .potentiallyFulfillingCollectionLoops - .add( - new PotentiallyFulfillingCollectionLoop( - collectionTree, - collectionElementTree, - conditionTree, - loopBodyEntryBlock, - loopConditionalBlock, - collectionElementNode)); - } - - /** - * Records an enhanced-for loop that matched syntactically and still needs CFG resolution. - * - * @param enclosingMethodTree enclosing method that contains the loop - * @param enhancedForLoopTree matched enhanced-for loop - */ - public void recordPotentiallyFulfillingEnhancedForLoop( - MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { - getOrCreateMethodCollectionLoopState(enclosingMethodTree) - .potentiallyFulfillingEnhancedForLoops - .add(enhancedForLoopTree); + @Override + protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { + if (ast.getKind() == UnderlyingAST.Kind.METHOD) { + ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this).prepareDisposalLoops(cfg); + } } - /** - * Records a collection loop whose CFG facts are fully resolved for the consistency analyzer. - * - * @param enclosingMethodTree enclosing method that contains the loop - * @param collectionTree collection expression whose elements may be discharged - * @param collectionElementTree tree for the element extracted from the collection - * @param conditionTree loop condition tree - * @param loopBodyEntryBlock first block of the loop body - * @param loopUpdateBlock block that updates the loop state before the next iteration - * @param loopConditionalBlock conditional block that controls the loop - * @param collectionElementNode CFG node for the extracted collection element - */ - public void recordResolvedPotentiallyFulfillingCollectionLoop( - MethodTree enclosingMethodTree, - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree conditionTree, - Block loopBodyEntryBlock, - Block loopUpdateBlock, - ConditionalBlock loopConditionalBlock, - Node collectionElementNode) { - getOrCreateMethodCollectionLoopState(enclosingMethodTree) - .resolvedPotentiallyFulfillingCollectionLoops - .add( - new ResolvedPotentiallyFulfillingCollectionLoop( - collectionTree, - collectionElementTree, - conditionTree, - loopBodyEntryBlock, - loopUpdateBlock, - loopConditionalBlock, - collectionElementNode)); - } + // /** + // * Records a while-like collection loop that matched syntactically and still needs CFG + // resolution. + // * + // * @param enclosingMethodTree enclosing method that contains the loop + // * @param collectionTree collection expression whose elements may be discharged + // * @param collectionElementTree tree for the element extracted from the collection + // * @param conditionTree loop condition tree + // * @param loopBodyEntryBlock first block of the loop body + // * @param loopConditionalBlock conditional block that controls the loop + // * @param collectionElementNode CFG node for the extracted collection element + // */ + // public void recordPotentiallyFulfillingCollectionLoop( + // MethodTree enclosingMethodTree, + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree conditionTree, + // Block loopBodyEntryBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionElementNode) { + // getOrCreateMethodCollectionLoopState(enclosingMethodTree) + // .potentiallyFulfillingCollectionLoops + // .add( + // new PotentiallyFulfillingCollectionLoop( + // collectionTree, + // collectionElementTree, + // conditionTree, + // loopBodyEntryBlock, + // loopConditionalBlock, + // collectionElementNode)); + // } + + // /** + // * Records an enhanced-for loop that matched syntactically and still needs CFG resolution. + // * + // * @param enclosingMethodTree enclosing method that contains the loop + // * @param enhancedForLoopTree matched enhanced-for loop + // */ + // public void recordPotentiallyFulfillingEnhancedForLoop( + // MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { + // getOrCreateMethodCollectionLoopState(enclosingMethodTree) + // .potentiallyFulfillingEnhancedForLoops + // .add(enhancedForLoopTree); + // } + + // /** + // * Records a collection loop whose CFG facts are fully resolved for the consistency analyzer. + // * + // * @param enclosingMethodTree enclosing method that contains the loop + // * @param collectionTree collection expression whose elements may be discharged + // * @param collectionElementTree tree for the element extracted from the collection + // * @param conditionTree loop condition tree + // * @param loopBodyEntryBlock first block of the loop body + // * @param loopUpdateBlock block that updates the loop state before the next iteration + // * @param loopConditionalBlock conditional block that controls the loop + // * @param collectionElementNode CFG node for the extracted collection element + // */ + // public void recordResolvedPotentiallyFulfillingCollectionLoop( + // MethodTree enclosingMethodTree, + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree conditionTree, + // Block loopBodyEntryBlock, + // Block loopUpdateBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionElementNode) { + // getOrCreateMethodCollectionLoopState(enclosingMethodTree) + // .resolvedPotentiallyFulfillingCollectionLoops + // .add( + // new ResolvedPotentiallyFulfillingCollectionLoop( + // collectionTree, + // collectionElementTree, + // conditionTree, + // loopBodyEntryBlock, + // loopUpdateBlock, + // loopConditionalBlock, + // collectionElementNode)); + // } @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { @@ -1059,486 +1059,503 @@ public TransferInput getInput(Block block) return analysis.getInput(block); } } - - /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. */ - public static class PotentiallyFulfillingCollectionLoop { - - /** AST {@code Tree} for collection iterated over. */ - public final ExpressionTree collectionTree; - - /** AST {@code Tree} for collection element iterated over. */ - public final Tree collectionElementTree; - - /** AST {@code Tree} for loop condition. */ - public final Tree condition; - - /** cfg {@code Block} containing the loop body entry. */ - public final Block loopBodyEntryBlock; - - /** cfg conditional {@link Block} following loop condition. */ - public final ConditionalBlock loopConditionalBlock; - - /** cfg {@code Node} for the collection element iterated over. */ - public final Node collectionElementNode; - - /** - * Constructs a new {@code PotentiallyFulfillingCollectionLoop}. - * - * @param collectionTree AST {@link Tree} for collection iterated over - * @param collectionElementTree AST {@link Tree} for collection element iterated over - * @param condition AST {@link Tree} for loop condition - * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry - * @param loopConditionalBlock cfg conditional {@link Block} following loop condition - * @param collectionEltNode cfg {@link Node} for the collection element iterated over - */ - public PotentiallyFulfillingCollectionLoop( - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree condition, - Block loopBodyEntryBlock, - ConditionalBlock loopConditionalBlock, - Node collectionEltNode) { - this.collectionTree = collectionTree; - this.collectionElementTree = collectionElementTree; - this.condition = condition; - this.loopBodyEntryBlock = loopBodyEntryBlock; - this.loopConditionalBlock = loopConditionalBlock; - this.collectionElementNode = collectionEltNode; - } - } - - /** - * A potentially fulfilling collection loop whose CFG-local information is complete enough for - * consistency analysis. - */ - public static class ResolvedPotentiallyFulfillingCollectionLoop - extends PotentiallyFulfillingCollectionLoop { - - /** - * The methods that the loop definitely calls on all elements of the collection it iterates - * over. - */ - protected final Set calledMethods; - - /** cfg {@code Block} containing the loop update. */ - public final Block loopUpdateBlock; - - /** - * Constructs a new {@code ResolvedPotentiallyFulfillingCollectionLoop}. - * - * @param collectionTree AST {@link Tree} for collection iterated over - * @param collectionElementTree AST {@link Tree} for collection element iterated over - * @param condition AST {@link Tree} for loop condition - * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry - * @param loopUpdateBlock cfg {@link Block} for the loop update - * @param loopConditionalBlock cfg conditional {@link Block} following loop condition - * @param collectionEltNode cfg {@link Node} for the collection element iterated over - */ - public ResolvedPotentiallyFulfillingCollectionLoop( - ExpressionTree collectionTree, - Tree collectionElementTree, - Tree condition, - Block loopBodyEntryBlock, - Block loopUpdateBlock, - ConditionalBlock loopConditionalBlock, - Node collectionEltNode) { - super( - collectionTree, - collectionElementTree, - condition, - loopBodyEntryBlock, - loopConditionalBlock, - collectionEltNode); - this.calledMethods = new HashSet<>(); - this.loopUpdateBlock = loopUpdateBlock; - } - - /** - * Add methods that are guaranteed to be invoked on every element of the collection the loop - * iterates over. - * - * @param methods the set of methods to add - */ - public void addCalledMethods(Set methods) { - calledMethods.addAll(methods); - } - - /** - * Returns methods that are guaranteed to be invoked on every element of the collection the loop - * iterates over. - * - * @return the set of methods the loop calls on all elements of the iterated collection - */ - public Set getCalledMethods() { - return calledMethods; - } - } - - /** - * After running the called-methods analysis, call the consistency analyzer to analyze - * CFG-resolved potentially fulfilling collection loops, as determined by a pre-pattern-match in - * the MustCallVisitor. - * - *

The analysis uses the CalledMethods type of the collection element iterated over to - * determine the methods the loop calls on the collection elements. - * - * @param cfg the cfg of the enclosing method - */ - @Override - public void postAnalyze(ControlFlowGraph cfg) { - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = - new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); - MethodCollectionLoopState loopState = getMethodCollectionLoopState(cfg.getUnderlyingAST()); - - if (loopState != null) { - if (!loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) { - new EnhancedForLoopResolver(cfg, loopState).resolveEnhancedForLoops(); - } - new WhileLoopResolver(cfg).resolveWhileLoops(loopState); - analyzeResolvedPotentiallyFulfillingCollectionLoops( - cfg, loopState, mustCallConsistencyAnalyzer); - } - - super.postAnalyze(cfg); - removeMethodCollectionLoopState(cfg.getUnderlyingAST()); - } - - /** - * Returns blocks reachable from {@code entryBlock}. - * - *

This remains a utility on the outer type because the resource leak consistency analyzer also - * uses it. - * - * @param entryBlock the CFG entry block - * @return the reachable blocks - */ - public static Set reachableFrom(Block entryBlock) { - return WhileLoopResolutionCache.reachableFrom(entryBlock); - } - - /** - * Analyzes CFG-resolved potentially fulfilling collection loops for the current method and - * removes the ones that were analyzed. - * - * @param cfg the CFG of the current method - * @param loopState per-method collection-loop state - * @param mustCallConsistencyAnalyzer the consistency analyzer - */ - private void analyzeResolvedPotentiallyFulfillingCollectionLoops( - ControlFlowGraph cfg, - MethodCollectionLoopState loopState, - MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer) { - if (loopState.resolvedPotentiallyFulfillingCollectionLoops.isEmpty()) { - return; - } - - Iterator resolvedLoopIterator = - loopState.resolvedPotentiallyFulfillingCollectionLoops.iterator(); - while (resolvedLoopIterator.hasNext()) { - ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = resolvedLoopIterator.next(); - Tree collectionElementTree = resolvedLoop.collectionElementTree; - boolean loopContainedInThisMethod = - cfg.getNodesCorrespondingToTree(collectionElementTree) != null; - if (loopContainedInThisMethod) { - Set disposalLoopCalledMethods = - mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, resolvedLoop); - if (disposalLoopCalledMethods != null) { - ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this) - .registerCalledMethodsForDisposalLoop(resolvedLoop, disposalLoopCalledMethods); - } - resolvedLoopIterator.remove(); - } - } - } - - /** Resolves enhanced-for-loop candidates into CFG-resolved loops for consistency analysis. */ - private final class EnhancedForLoopResolver { - /** The CFG of the current method. */ - private final ControlFlowGraph cfg; - - /** Per-method collection-loop state. */ - private final MethodCollectionLoopState loopState; - - /** Blocks that have already been visited while traversing the CFG. */ - private final Set visitedBlocks = new HashSet<>(); - - /** Worklist for CFG traversal. */ - private final Deque worklist = new ArrayDeque<>(); - - /** - * Creates a resolver for enhanced-for-loops in the given CFG. - * - * @param cfg the CFG of the current method - * @param loopState per-method collection-loop state - */ - private EnhancedForLoopResolver(ControlFlowGraph cfg, MethodCollectionLoopState loopState) { - this.cfg = cfg; - this.loopState = loopState; - } - - /** Traverses the CFG and records resolved enhanced-for-loops for the pending candidates. */ - private void resolveEnhancedForLoops() { - Block entryBlock = cfg.getEntryBlock(); - worklist.add(entryBlock); - visitedBlocks.add(entryBlock); - - while (!worklist.isEmpty() && !loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) { - Block currentBlock = worklist.removeFirst(); - - for (Node node : currentBlock.getNodes()) { - if (node instanceof MethodInvocationNode) { - resolveEnhancedForLoop((MethodInvocationNode) node); - } - } - - for (IPair successorAndExceptionType : - getSuccessorsExceptIgnoredExceptions(currentBlock)) { - Block successorBlock = successorAndExceptionType.first; - if (successorBlock != null && visitedBlocks.add(successorBlock)) { - worklist.addLast(successorBlock); - } - } - } - } - - /** - * Returns all successor blocks for some block, except for those corresponding to ignored - * exception types. See {@link RLCCalledMethodsAnalysis#isIgnoredExceptionType(TypeMirror)}. - * - * @param block input block - * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for - * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor - */ - private Set> getSuccessorsExceptIgnoredExceptions( - Block block) { - if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { - ExceptionBlock exceptionBlock = (ExceptionBlock) block; - Set> result = new LinkedHashSet<>(); - Block regularSuccessor = exceptionBlock.getSuccessor(); - if (regularSuccessor != null) { - result.add(IPair.of(regularSuccessor, null)); - } - Map> exceptionalSuccessors = - exceptionBlock.getExceptionalSuccessors(); - for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { - TypeMirror exceptionType = entry.getKey(); - if (!isIgnoredExceptionType(exceptionType)) { - for (Block exceptionalSuccessor : entry.getValue()) { - result.add(IPair.of(exceptionalSuccessor, exceptionType)); - } - } - } - return result; - } else { - Set> result = new LinkedHashSet<>(); - for (Block successorBlock : block.getSuccessors()) { - result.add(IPair.of(successorBlock, null)); - } - return result; - } - } - - /** - * Records a resolved collection loop if the given node is desugared from an enhanced-for-loop - * over a collection. - * - * @param methodInvocationNode the node to check - */ - private void resolveEnhancedForLoop(MethodInvocationNode methodInvocationNode) { - if (methodInvocationNode.getIterableExpression() == null) { - return; - } - - EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); - if (loop == null) { - throw new BugInCF( - "MethodInvocationNode.iterableExpression should be non-null iff" - + " MethodInvocationNode.enhancedForLoop is non-null"); - } - if (!loopState.potentiallyFulfillingEnhancedForLoops.contains(loop)) { - return; - } - - VariableTree loopVariable = loop.getVariable(); - - // Find the first block of the loop body by traversing the desugared iterator.next() path - // until the assignment of the loop variable is found. - SingleSuccessorBlock singleSuccessorBlock = - (SingleSuccessorBlock) methodInvocationNode.getBlock(); - Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); - Node loopVariableNode = null; - Node node; - boolean isAssignmentOfLoopVariable; - do { - while (!nodeIterator.hasNext()) { - singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); - nodeIterator = singleSuccessorBlock.getNodes().iterator(); - } - node = nodeIterator.next(); - isAssignmentOfLoopVariable = false; - if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { - loopVariableNode = ((AssignmentNode) node).getTarget(); - VariableTree iteratorVariableDeclaration = (VariableTree) node.getTree(); - isAssignmentOfLoopVariable = - iteratorVariableDeclaration.getName() == loopVariable.getName(); - } - } while (!isAssignmentOfLoopVariable); - Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); - - // Find the desugared loop condition by traversing the CFG backwards until iterator.hasNext() - // is found. - Block loopUpdateBlock = methodInvocationNode.getBlock(); - nodeIterator = loopUpdateBlock.getNodes().iterator(); - boolean isLoopCondition; - do { - while (!nodeIterator.hasNext()) { - Set predecessorBlocks = loopUpdateBlock.getPredecessors(); - if (predecessorBlocks.size() == 1) { - loopUpdateBlock = predecessorBlocks.iterator().next(); - nodeIterator = loopUpdateBlock.getNodes().iterator(); - } else { - // There is no trivial resolution here. Best we can do is skip this loop. - return; - } - } - node = nodeIterator.next(); - isLoopCondition = false; - if (node instanceof MethodInvocationNode) { - MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); - isLoopCondition = TreeUtils.isHasNextCall(methodInvocationTree); - } - } while (!isLoopCondition); - - Block blockContainingLoopCondition = node.getBlock(); - if (blockContainingLoopCondition.getSuccessors().size() != 1) { - throw new BugInCF( - "loop condition has: " - + blockContainingLoopCondition.getSuccessors().size() - + " successors instead of 1."); - } - Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); - if (!(conditionalBlock instanceof ConditionalBlock)) { - throw new BugInCF( - "loop condition successor is not ConditionalBlock, but: " - + conditionalBlock.getClass()); - } - - loopState.resolvedPotentiallyFulfillingCollectionLoops.add( - new ResolvedPotentiallyFulfillingCollectionLoop( - loop.getExpression(), - loopVariableNode.getTree(), - node.getTree(), - loopBodyEntryBlock, - loopUpdateBlock, - (ConditionalBlock) conditionalBlock, - loopVariableNode)); - loopState.potentiallyFulfillingEnhancedForLoops.remove(loop); - } - } - - /** Resolves while-loop candidates into CFG-resolved loops for consistency analysis. */ - private static final class WhileLoopResolver { - /** The CFG of the current method. */ - private final ControlFlowGraph cfg; - - /** - * Creates a resolver for potentially fulfilling while loops in the given CFG. - * - * @param cfg the enclosing method CFG - */ - private WhileLoopResolver(ControlFlowGraph cfg) { - this.cfg = cfg; - } - - /** - * Resolves all potentially fulfilling while loops in the given method state that can be tied to - * a loop update block in the current CFG. - * - * @param loopState per-method collection-loop state - */ - private void resolveWhileLoops(MethodCollectionLoopState loopState) { - if (loopState.potentiallyFulfillingCollectionLoops.isEmpty()) { - return; - } - - WhileLoopResolutionCache whileLoopCache = loopState.getOrCreateWhileLoopCache(cfg); - - Iterator potentialLoopIterator = - loopState.potentiallyFulfillingCollectionLoops.iterator(); - while (potentialLoopIterator.hasNext()) { - PotentiallyFulfillingCollectionLoop potentialLoop = potentialLoopIterator.next(); - ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = - resolveWhileLoop(potentialLoop, whileLoopCache); - if (resolvedLoop != null) { - loopState.resolvedPotentiallyFulfillingCollectionLoops.add(resolvedLoop); - potentialLoopIterator.remove(); - } - } - } - - /** - * Resolves one potentially fulfilling while loop if a suitable loop update block can be found. - * - * @param potentialLoop a potentially fulfilling while loop - * @param whileLoopCache cached CFG facts for while-loop resolution - * @return the CFG-resolved loop, or {@code null} if no loop update block is found - */ - private @Nullable ResolvedPotentiallyFulfillingCollectionLoop resolveWhileLoop( - PotentiallyFulfillingCollectionLoop potentialLoop, - WhileLoopResolutionCache whileLoopCache) { - Block loopUpdateBlock = - chooseLoopUpdateBlockForPotentiallyFulfillingLoop(potentialLoop, whileLoopCache); - if (loopUpdateBlock == null) { - return null; - } - return new ResolvedPotentiallyFulfillingCollectionLoop( - potentialLoop.collectionTree, - potentialLoop.collectionElementTree, - potentialLoop.condition, - potentialLoop.loopBodyEntryBlock, - loopUpdateBlock, - potentialLoop.loopConditionalBlock, - potentialLoop.collectionElementNode); - } - - /** - * Chooses the best loop update block for a potentially fulfilling while loop by matching it to - * the tightest natural loop that contains both the body entry and the loop condition. - * - * @param potentialLoop a potentially fulfilling while loop - * @param whileLoopCache cached CFG facts for while-loop resolution - * @return the chosen loop update block, or {@code null} if none is found - */ - private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( - PotentiallyFulfillingCollectionLoop potentialLoop, - WhileLoopResolutionCache whileLoopCache) { - - Block bodyEntryBlock = potentialLoop.loopBodyEntryBlock; - Block conditionalBlock = potentialLoop.loopConditionalBlock; - - Block bestLoopUpdateBlock = null; - int bestLoopSize = Integer.MAX_VALUE; - - for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { - // backEdge.targetBlock is the candidate block that the loop body flows back to. - Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); - - // Must contain this while-loop's body entry and conditional block. - if (!naturalLoop.contains(bodyEntryBlock)) { - continue; - } - if (!naturalLoop.contains(conditionalBlock)) { - continue; - } - - // Prefer the tightest loop. This helps nested-loop disambiguation. - if (naturalLoop.size() < bestLoopSize) { - bestLoopSize = naturalLoop.size(); - bestLoopUpdateBlock = backEdge.targetBlock; - } - } - - return bestLoopUpdateBlock; - } - } + // + // /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. + // */ + // public static class PotentiallyFulfillingCollectionLoop { + // + // /** AST {@code Tree} for collection iterated over. */ + // public final ExpressionTree collectionTree; + // + // /** AST {@code Tree} for collection element iterated over. */ + // public final Tree collectionElementTree; + // + // /** AST {@code Tree} for loop condition. */ + // public final Tree condition; + // + // /** cfg {@code Block} containing the loop body entry. */ + // public final Block loopBodyEntryBlock; + // + // /** cfg conditional {@link Block} following loop condition. */ + // public final ConditionalBlock loopConditionalBlock; + // + // /** cfg {@code Node} for the collection element iterated over. */ + // public final Node collectionElementNode; + // + // /** + // * Constructs a new {@code PotentiallyFulfillingCollectionLoop}. + // * + // * @param collectionTree AST {@link Tree} for collection iterated over + // * @param collectionElementTree AST {@link Tree} for collection element iterated over + // * @param condition AST {@link Tree} for loop condition + // * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry + // * @param loopConditionalBlock cfg conditional {@link Block} following loop condition + // * @param collectionEltNode cfg {@link Node} for the collection element iterated over + // */ + // public PotentiallyFulfillingCollectionLoop( + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree condition, + // Block loopBodyEntryBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionEltNode) { + // this.collectionTree = collectionTree; + // this.collectionElementTree = collectionElementTree; + // this.condition = condition; + // this.loopBodyEntryBlock = loopBodyEntryBlock; + // this.loopConditionalBlock = loopConditionalBlock; + // this.collectionElementNode = collectionEltNode; + // } + // } + // + // /** + // * A potentially fulfilling collection loop whose CFG-local information is complete enough for + // * consistency analysis. + // */ + // public static class ResolvedPotentiallyFulfillingCollectionLoop + // extends PotentiallyFulfillingCollectionLoop { + // + // /** + // * The methods that the loop definitely calls on all elements of the collection it iterates + // * over. + // */ + // protected final Set calledMethods; + // + // /** cfg {@code Block} containing the loop update. */ + // public final Block loopUpdateBlock; + // + // /** + // * Constructs a new {@code ResolvedPotentiallyFulfillingCollectionLoop}. + // * + // * @param collectionTree AST {@link Tree} for collection iterated over + // * @param collectionElementTree AST {@link Tree} for collection element iterated over + // * @param condition AST {@link Tree} for loop condition + // * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry + // * @param loopUpdateBlock cfg {@link Block} for the loop update + // * @param loopConditionalBlock cfg conditional {@link Block} following loop condition + // * @param collectionEltNode cfg {@link Node} for the collection element iterated over + // */ + // public ResolvedPotentiallyFulfillingCollectionLoop( + // ExpressionTree collectionTree, + // Tree collectionElementTree, + // Tree condition, + // Block loopBodyEntryBlock, + // Block loopUpdateBlock, + // ConditionalBlock loopConditionalBlock, + // Node collectionEltNode) { + // super( + // collectionTree, + // collectionElementTree, + // condition, + // loopBodyEntryBlock, + // loopConditionalBlock, + // collectionEltNode); + // this.calledMethods = new HashSet<>(); + // this.loopUpdateBlock = loopUpdateBlock; + // } + + // /** + // * Add methods that are guaranteed to be invoked on every element of the collection the loop + // * iterates over. + // * + // * @param methods the set of methods to add + // */ + // public void addCalledMethods(Set methods) { + // calledMethods.addAll(methods); + // } + + // /** + // * Returns methods that are guaranteed to be invoked on every element of the collection the + // loop + // * iterates over. + // * + // * @return the set of methods the loop calls on all elements of the iterated collection + // */ + // public Set getCalledMethods() { + // return calledMethods; + // } + // } + + // /** + // * After running the called-methods analysis, call the consistency analyzer to analyze + // * CFG-resolved potentially fulfilling collection loops, as determined by a pre-pattern-match + // in + // * the MustCallVisitor. + // * + // *

The analysis uses the CalledMethods type of the collection element iterated over to + // * determine the methods the loop calls on the collection elements. + // * + // * @param cfg the cfg of the enclosing method + // */ + // @Override + // protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { + // // Disposal-loop certification now runs in + // // CollectionOwnershipAnnotatedTypeFactory.postCFGConstruction(...), after RLCC analysis and + // // before CO analysis. + // } + + // @Override + // public void postAnalyze(ControlFlowGraph cfg) { + // super.postAnalyze(cfg); + // removeMethodCollectionLoopState(cfg.getUnderlyingAST()); + // } + + // /** + // * Returns blocks reachable from {@code entryBlock}. + // * + // *

This remains a utility on the outer type because the resource leak consistency analyzer + // also + // * uses it. + // * + // * @param entryBlock the CFG entry block + // * @return the reachable blocks + // */ + // public static Set reachableFrom(Block entryBlock) { + // return WhileLoopResolutionCache.reachableFrom(entryBlock); + // } + + // /** + // * Analyzes CFG-resolved potentially fulfilling collection loops for the current method and + // * removes the ones that were analyzed. + // * + // * @param cfg the CFG of the current method + // * @param loopState per-method collection-loop state + // * @param mustCallConsistencyAnalyzer the consistency analyzer + // */ + // @SuppressWarnings("UnusedMethod") + // private void analyzeResolvedPotentiallyFulfillingCollectionLoops( + // ControlFlowGraph cfg, + // MethodCollectionLoopState loopState, + // MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer) { + // if (loopState.resolvedPotentiallyFulfillingCollectionLoops.isEmpty()) { + // return; + // } + // + // Iterator resolvedLoopIterator = + // loopState.resolvedPotentiallyFulfillingCollectionLoops.iterator(); + // while (resolvedLoopIterator.hasNext()) { + // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = resolvedLoopIterator.next(); + // Tree collectionElementTree = resolvedLoop.collectionElementTree; + // boolean loopContainedInThisMethod = + // cfg.getNodesCorrespondingToTree(collectionElementTree) != null; + // if (loopContainedInThisMethod) { + // DisposalLoop disposalLoop = + // new DisposalLoop( + // resolvedLoop.collectionTree, + // resolvedLoop.collectionElementTree, + // resolvedLoop.collectionElementNode, + // resolvedLoop.condition, + // resolvedLoop.loopConditionalBlock, + // resolvedLoop.loopBodyEntryBlock, + // resolvedLoop.loopUpdateBlock); + // Set disposalLoopCalledMethods = + // mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoop); + // if (disposalLoopCalledMethods != null) { + // ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this) + // .registerCalledMethodsForDisposalLoop(resolvedLoop, disposalLoopCalledMethods); + // } + // resolvedLoopIterator.remove(); + // } + // } + // } + // + // /** Resolves enhanced-for-loop candidates into CFG-resolved loops for consistency analysis. */ + // @SuppressWarnings({"UnusedNestedClass", "UnusedMethod"}) + // private final class EnhancedForLoopResolver { + // /** The CFG of the current method. */ + // private final ControlFlowGraph cfg; + // + // /** Per-method collection-loop state. */ + // private final MethodCollectionLoopState loopState; + // + // /** Blocks that have already been visited while traversing the CFG. */ + // private final Set visitedBlocks = new HashSet<>(); + // + // /** Worklist for CFG traversal. */ + // private final Deque worklist = new ArrayDeque<>(); + // + // /** + // * Creates a resolver for enhanced-for-loops in the given CFG. + // * + // * @param cfg the CFG of the current method + // * @param loopState per-method collection-loop state + // */ + // private EnhancedForLoopResolver(ControlFlowGraph cfg, MethodCollectionLoopState loopState) { + // this.cfg = cfg; + // this.loopState = loopState; + // } + // + // /** Traverses the CFG and records resolved enhanced-for-loops for the pending candidates. */ + // private void resolveEnhancedForLoops() { + // Block entryBlock = cfg.getEntryBlock(); + // worklist.add(entryBlock); + // visitedBlocks.add(entryBlock); + // + // while (!worklist.isEmpty() && !loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) + // { + // Block currentBlock = worklist.removeFirst(); + // + // for (Node node : currentBlock.getNodes()) { + // if (node instanceof MethodInvocationNode) { + // resolveEnhancedForLoop((MethodInvocationNode) node); + // } + // } + // + // for (IPair successorAndExceptionType : + // getSuccessorsExceptIgnoredExceptions(currentBlock)) { + // Block successorBlock = successorAndExceptionType.first; + // if (successorBlock != null && visitedBlocks.add(successorBlock)) { + // worklist.addLast(successorBlock); + // } + // } + // } + // } + // + // /** + // * Returns all successor blocks for some block, except for those corresponding to ignored + // * exception types. See {@link RLCCalledMethodsAnalysis#isIgnoredExceptionType(TypeMirror)}. + // * + // * @param block input block + // * @return set of pairs (b, t), where b is a successor block, and t is the type of exception + // for + // * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor + // */ + // private Set> getSuccessorsExceptIgnoredExceptions( + // Block block) { + // if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { + // ExceptionBlock exceptionBlock = (ExceptionBlock) block; + // Set> result = new LinkedHashSet<>(); + // Block regularSuccessor = exceptionBlock.getSuccessor(); + // if (regularSuccessor != null) { + // result.add(IPair.of(regularSuccessor, null)); + // } + // Map> exceptionalSuccessors = + // exceptionBlock.getExceptionalSuccessors(); + // for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { + // TypeMirror exceptionType = entry.getKey(); + // if (!isIgnoredExceptionType(exceptionType)) { + // for (Block exceptionalSuccessor : entry.getValue()) { + // result.add(IPair.of(exceptionalSuccessor, exceptionType)); + // } + // } + // } + // return result; + // } else { + // Set> result = new LinkedHashSet<>(); + // for (Block successorBlock : block.getSuccessors()) { + // result.add(IPair.of(successorBlock, null)); + // } + // return result; + // } + // } + + // /** + // * Records a resolved collection loop if the given node is desugared from an + // enhanced-for-loop + // * over a collection. + // * + // * @param methodInvocationNode the node to check + // */ + // private void resolveEnhancedForLoop(MethodInvocationNode methodInvocationNode) { + // if (methodInvocationNode.getIterableExpression() == null) { + // return; + // } + // + // EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); + // if (loop == null) { + // throw new BugInCF( + // "MethodInvocationNode.iterableExpression should be non-null iff" + // + " MethodInvocationNode.enhancedForLoop is non-null"); + // } + // if (!loopState.potentiallyFulfillingEnhancedForLoops.contains(loop)) { + // return; + // } + // + // VariableTree loopVariable = loop.getVariable(); + // + // // Find the first block of the loop body by traversing the desugared iterator.next() path + // // until the assignment of the loop variable is found. + // SingleSuccessorBlock singleSuccessorBlock = + // (SingleSuccessorBlock) methodInvocationNode.getBlock(); + // Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); + // Node loopVariableNode = null; + // Node node; + // boolean isAssignmentOfLoopVariable; + // do { + // while (!nodeIterator.hasNext()) { + // singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); + // nodeIterator = singleSuccessorBlock.getNodes().iterator(); + // } + // node = nodeIterator.next(); + // isAssignmentOfLoopVariable = false; + // if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { + // loopVariableNode = ((AssignmentNode) node).getTarget(); + // VariableTree iteratorVariableDeclaration = (VariableTree) node.getTree(); + // isAssignmentOfLoopVariable = + // iteratorVariableDeclaration.getName() == loopVariable.getName(); + // } + // } while (!isAssignmentOfLoopVariable); + // Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); + // + // // Find the desugared loop condition by traversing the CFG backwards until + // iterator.hasNext() + // // is found. + // Block loopUpdateBlock = methodInvocationNode.getBlock(); + // nodeIterator = loopUpdateBlock.getNodes().iterator(); + // boolean isLoopCondition; + // do { + // while (!nodeIterator.hasNext()) { + // Set predecessorBlocks = loopUpdateBlock.getPredecessors(); + // if (predecessorBlocks.size() == 1) { + // loopUpdateBlock = predecessorBlocks.iterator().next(); + // nodeIterator = loopUpdateBlock.getNodes().iterator(); + // } else { + // // There is no trivial resolution here. Best we can do is skip this loop. + // return; + // } + // } + // node = nodeIterator.next(); + // isLoopCondition = false; + // if (node instanceof MethodInvocationNode) { + // MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); + // isLoopCondition = TreeUtils.isHasNextCall(methodInvocationTree); + // } + // } while (!isLoopCondition); + // + // Block blockContainingLoopCondition = node.getBlock(); + // if (blockContainingLoopCondition.getSuccessors().size() != 1) { + // throw new BugInCF( + // "loop condition has: " + // + blockContainingLoopCondition.getSuccessors().size() + // + " successors instead of 1."); + // } + // Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + // if (!(conditionalBlock instanceof ConditionalBlock)) { + // throw new BugInCF( + // "loop condition successor is not ConditionalBlock, but: " + // + conditionalBlock.getClass()); + // } + // + // loopState.resolvedPotentiallyFulfillingCollectionLoops.add( + // new ResolvedPotentiallyFulfillingCollectionLoop( + // loop.getExpression(), + // loopVariableNode.getTree(), + // node.getTree(), + // loopBodyEntryBlock, + // loopUpdateBlock, + // (ConditionalBlock) conditionalBlock, + // loopVariableNode)); + // loopState.potentiallyFulfillingEnhancedForLoops.remove(loop); + // } + // } + // + // /** Resolves while-loop candidates into CFG-resolved loops for consistency analysis. */ + // @SuppressWarnings({"UnusedNestedClass", "UnusedMethod"}) + // private static final class WhileLoopResolver { + // /** The CFG of the current method. */ + // private final ControlFlowGraph cfg; + // + // /** + // * Creates a resolver for potentially fulfilling while loops in the given CFG. + // * + // * @param cfg the enclosing method CFG + // */ + // private WhileLoopResolver(ControlFlowGraph cfg) { + // this.cfg = cfg; + // } + // + // /** + // * Resolves all potentially fulfilling while loops in the given method state that can be + // tied to + // * a loop update block in the current CFG. + // * + // * @param loopState per-method collection-loop state + // */ + // private void resolveWhileLoops(MethodCollectionLoopState loopState) { + // if (loopState.potentiallyFulfillingCollectionLoops.isEmpty()) { + // return; + // } + // + // WhileLoopResolutionCache whileLoopCache = loopState.getOrCreateWhileLoopCache(cfg); + // + // Iterator potentialLoopIterator = + // loopState.potentiallyFulfillingCollectionLoops.iterator(); + // while (potentialLoopIterator.hasNext()) { + // PotentiallyFulfillingCollectionLoop potentialLoop = potentialLoopIterator.next(); + // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = + // resolveWhileLoop(potentialLoop, whileLoopCache); + // if (resolvedLoop != null) { + // loopState.resolvedPotentiallyFulfillingCollectionLoops.add(resolvedLoop); + // potentialLoopIterator.remove(); + // } + // } + // } + // + // /** + // * Resolves one potentially fulfilling while loop if a suitable loop update block can be + // found. + // * + // * @param potentialLoop a potentially fulfilling while loop + // * @param whileLoopCache cached CFG facts for while-loop resolution + // * @return the CFG-resolved loop, or {@code null} if no loop update block is found + // */ + // private @Nullable ResolvedPotentiallyFulfillingCollectionLoop resolveWhileLoop( + // PotentiallyFulfillingCollectionLoop potentialLoop, + // WhileLoopResolutionCache whileLoopCache) { + // Block loopUpdateBlock = + // chooseLoopUpdateBlockForPotentiallyFulfillingLoop(potentialLoop, whileLoopCache); + // if (loopUpdateBlock == null) { + // return null; + // } + // return new ResolvedPotentiallyFulfillingCollectionLoop( + // potentialLoop.collectionTree, + // potentialLoop.collectionElementTree, + // potentialLoop.condition, + // potentialLoop.loopBodyEntryBlock, + // loopUpdateBlock, + // potentialLoop.loopConditionalBlock, + // potentialLoop.collectionElementNode); + // } + // + // /** + // * Chooses the best loop update block for a potentially fulfilling while loop by matching it + // to + // * the tightest natural loop that contains both the body entry and the loop condition. + // * + // * @param potentialLoop a potentially fulfilling while loop + // * @param whileLoopCache cached CFG facts for while-loop resolution + // * @return the chosen loop update block, or {@code null} if none is found + // */ + // private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( + // PotentiallyFulfillingCollectionLoop potentialLoop, + // WhileLoopResolutionCache whileLoopCache) { + // + // Block bodyEntryBlock = potentialLoop.loopBodyEntryBlock; + // Block conditionalBlock = potentialLoop.loopConditionalBlock; + // + // Block bestLoopUpdateBlock = null; + // int bestLoopSize = Integer.MAX_VALUE; + // + // for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { + // // backEdge.targetBlock is the candidate block that the loop body flows back to. + // Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); + // + // // Must contain this while-loop's body entry and conditional block. + // if (!naturalLoop.contains(bodyEntryBlock)) { + // continue; + // } + // if (!naturalLoop.contains(conditionalBlock)) { + // continue; + // } + // + // // Prefer the tightest loop. This helps nested-loop disambiguation. + // if (naturalLoop.size() < bestLoopSize) { + // bestLoopSize = naturalLoop.size(); + // bestLoopUpdateBlock = backEdge.targetBlock; + // } + // } + // + // return bestLoopUpdateBlock; + // } + // } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 2670ea25ace6..f9c00abf741d 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -9,10 +9,12 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.CalledMethodsTransfer; +import org.checkerframework.checker.collectionownership.DisposalLoop; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer; +import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.common.accumulation.AccumulationStore; import org.checkerframework.common.accumulation.AccumulationValue; import org.checkerframework.dataflow.analysis.TransferInput; @@ -67,25 +69,13 @@ public void accumulate( public AccumulationStore initialStore( UnderlyingAST underlyingAST, List parameters) { AccumulationStore store = super.initialStore(underlyingAST, parameters); - RLCCalledMethodsAnnotatedTypeFactory cmAtf = - (RLCCalledMethodsAnnotatedTypeFactory) this.analysis.getTypeFactory(); - for (RLCCalledMethodsAnnotatedTypeFactory.PotentiallyFulfillingCollectionLoop - potentiallyFulfillingCollectionLoop : - cmAtf.getPotentiallyFulfillingCollectionLoops(underlyingAST)) { + for (DisposalLoop disposalLoop : + ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(rlTypeFactory) + .getPreparedDisposalLoops(underlyingAST)) { IteratedCollectionElement collectionElementJE = new IteratedCollectionElement( - potentiallyFulfillingCollectionLoop.collectionElementNode, - potentiallyFulfillingCollectionLoop.collectionElementTree); - store.insertValue(collectionElementJE, cmAtf.top); - } - for (RLCCalledMethodsAnnotatedTypeFactory.ResolvedPotentiallyFulfillingCollectionLoop - resolvedPotentiallyFulfillingLoop : - cmAtf.getResolvedPotentiallyFulfillingCollectionLoops(underlyingAST)) { - IteratedCollectionElement collectionElementJE = - new IteratedCollectionElement( - resolvedPotentiallyFulfillingLoop.collectionElementNode, - resolvedPotentiallyFulfillingLoop.collectionElementTree); - store.insertValue(collectionElementJE, cmAtf.top); + disposalLoop.iteratedElementNode, disposalLoop.iteratedElementTree); + store.insertValue(collectionElementJE, rlTypeFactory.top); } return store; } From 9512f05135cf44db6d3dbebad5f0c564d91cf51e Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Mon, 27 Apr 2026 15:14:48 -0700 Subject: [PATCH 363/374] old lambda workaround for RLCC. --- ...llectionOwnershipAnnotatedTypeFactory.java | 55 +++++++++++++------ .../RLCCalledMethodsAnnotatedTypeFactory.java | 37 +++---------- .../type/GenericAnnotatedTypeFactory.java | 18 ------ 3 files changed, 48 insertions(+), 62 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index 736507538c43..cb6f9bbc5b9e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -1,6 +1,8 @@ package org.checkerframework.checker.collectionownership; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -15,6 +17,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -50,6 +53,7 @@ import org.checkerframework.dataflow.expression.FieldAccess; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.expression.JavaExpressionParseException; +import org.checkerframework.framework.flow.CFAbstractAnalysis.FieldInitialValue; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -66,6 +70,7 @@ import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.IPair; /** The annotated type factory for the Collection Ownership Checker. */ public class CollectionOwnershipAnnotatedTypeFactory @@ -409,28 +414,46 @@ public CollectionOwnershipStore getStoreForBlock( : flowResult.getStoreBefore(succBlock); } - /** - * Runs resource-leak post-analysis after the first method analysis and before any contained - * lambdas are analyzed. - * - *

This override exists because the Collection Ownership Checker currently runs last in the - * Resource Leak Checker hierarchy. The last checker in that hierarchy is responsible for - * triggering the method-level resource-leak post-analysis for the enclosing method. - * - * @param cfg the method CFG that has completed its first analysis - */ @Override - protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { - runResourceLeakPostAnalyze(cfg); - preLambdaPostAnalyzedMethods.add(cfg); + protected ControlFlowGraph analyze( + Queue> classQueue, + Queue> lambdaQueue, + UnderlyingAST ast, + List> fieldValues, + @Nullable ControlFlowGraph cfg, + boolean isInitializationCode, + boolean updateInitializationStore, + boolean isStatic, + @Nullable CollectionOwnershipStore capturedStore) { + ControlFlowGraph result = + super.analyze( + classQueue, + lambdaQueue, + ast, + fieldValues, + cfg, + isInitializationCode, + updateInitializationStore, + isStatic, + capturedStore); + if (cfg == null && ast.getKind() == UnderlyingAST.Kind.METHOD) { + // This uses the same RLLambda.java workaround pattern that originally lived in RLCC: + // run the resource-leak post-analysis immediately after the first method analysis, before + // containing lambdas are reanalyzed to fixpoint. CO owns that post-analysis now, so the + // workaround lives here. + runResourceLeakPostAnalyze(result); + preLambdaPostAnalyzedMethods.add(result); + } + return result; } /** * Performs post-analysis for the given CFG. * - *

If resource-leak-specific post-analysis already ran during {@link - * #postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph)}, then this method avoids running it a - * second time for the enclosing method. + *

If resource-leak-specific post-analysis already ran during the first method analysis in + * {@link #analyze(Queue, Queue, UnderlyingAST, List, ControlFlowGraph, boolean, boolean, boolean, + * CollectionOwnershipStore)}, then this method avoids running it a second time for the enclosing + * method. * * @param cfg the CFG to post-analyze */ diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 0f51703bf185..7fa3d2e8d599 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -506,17 +506,16 @@ protected ControlFlowGraph analyze( boolean updateInitializationStore, boolean isStatic, @Nullable AccumulationStore capturedStore) { + // This is a workaround for a bug that I tried and failed to fix. + // See checker/tests/resourceleak/RLLambda.java. + // This code really belongs in postAnalyze, but this code only works correctly when called + // after a method is analyzed the first time and before any containing lambdas are analyzed. + // This workaround means there could be false positives when the type of a method invocation + // depends on dataflow in a lambda. + if (cfg != null && ast.getKind() == UnderlyingAST.Kind.METHOD) { - // The old RLCC workaround for RLLambda.java (#7316) used to run in analyze(). It was moved - // to CollectionOwnershipAnnotatedTypeFactory.postAnalyzeAfterFirstMethodAnalysis(...) so it - // executes once at the correct lifecycle point: after the first method analysis and before - // lambda fixpoint. - // - // At this point cfg is the preserved first method analysis result. Keep that result, but - // re-enqueue nested classes and lambdas so later fixpoint iterations still analyze them. - // Returning cfg without re-enqueuing would incorrectly stop lambda processing; recomputing - // the method analysis would discard the preserved first-pass result that the early - // resource-leak post-analysis depends on. + // The cfg is not null, so the analysis has been run before. Keep that first result, but + // re-enqueue the nested classes and lambdas from the existing CFG so fixpoint still runs. for (ClassTree cls : cfg.getDeclaredClasses()) { classQueue.add(IPair.of(cls, getStoreBefore(cls))); } @@ -1176,24 +1175,6 @@ public TransferInput getInput(Block block) // } // } - // /** - // * After running the called-methods analysis, call the consistency analyzer to analyze - // * CFG-resolved potentially fulfilling collection loops, as determined by a pre-pattern-match - // in - // * the MustCallVisitor. - // * - // *

The analysis uses the CalledMethods type of the collection element iterated over to - // * determine the methods the loop calls on the collection elements. - // * - // * @param cfg the cfg of the enclosing method - // */ - // @Override - // protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) { - // // Disposal-loop certification now runs in - // // CollectionOwnershipAnnotatedTypeFactory.postCFGConstruction(...), after RLCC analysis and - // // before CO analysis. - // } - // @Override // public void postAnalyze(ControlFlowGraph cfg) { // super.postAnalyze(cfg); 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 23904c2c88a9..1276a950a8f9 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -1514,13 +1514,6 @@ private void performFlowAnalysisForMethod( /* updateInitializationStore= */ false, /* isStatic= */ false, capturedStore); - if (firstIteration) { - // Some checkers need a method-level pass after the method CFG is analyzed once but before - // contained lambdas enter fixpoint. This hook was added so such logic does not have to live - // inside analyze(); the Resource Leak Checker's old workaround for RLLambda.java (#7316) - // was the motivating case. - postAnalyzeAfterFirstMethodAnalysis(methodCFG); - } boolean anyLambdaResultChanged = false; while (!lambdaQueueInMethod.isEmpty()) { IPair lambdaPair = lambdaQueueInMethod.remove(); @@ -1587,17 +1580,6 @@ private boolean containsAllVoidLambdas(Set lambdas) { return true; } - /** - * Perform any additional operations on a method CFG after its first analysis and before any - * contained lambdas are analyzed. - * - *

This hook is invoked once per method CFG. If the method contains no lambdas, then this hook - * is called after the first analysis and before {@link #postAnalyze(ControlFlowGraph)}. - * - * @param cfg the method CFG - */ - protected void postAnalyzeAfterFirstMethodAnalysis(ControlFlowGraph cfg) {} - /** * Perform any additional operations after CFG construction, but before dataflow analysis runs on * the CFG. From b75ec8f57745738043ffbea048e6b716914a7b4b Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Mon, 27 Apr 2026 19:37:30 -0700 Subject: [PATCH 364/374] Adds documentation. --- .../CollectionOwnershipAnalysis.java | 7 +- ...llectionOwnershipAnnotatedTypeFactory.java | 207 ++-- .../CollectionOwnershipTransfer.java | 19 +- .../CollectionOwnershipUtils.java | 100 +- .../CollectionOwnershipVisitor.java | 15 +- .../collectionownership/DisposalLoop.java | 6 +- .../DisposalLoopScanner.java | 34 +- .../EnhancedForDisposalLoopResolver.java | 51 +- .../IndexedForDisposalLoopMatcher.java | 65 +- .../WhileDisposalLoopMatcher.java | 118 +- .../MustCallAnnotatedTypeFactory.java | 121 +- .../checker/mustcall/MustCallVisitor.java | 1070 ----------------- .../MustCallConsistencyAnalyzer.java | 89 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 928 +------------- .../RLCCalledMethodsTransfer.java | 6 +- ....java => ResourceLeakCollectionsTest.java} | 4 +- .../expression/IteratedCollectionElement.java | 6 +- .../framework/flow/CFAbstractStore.java | 9 +- .../checkerframework/javacutil/TreeUtils.java | 11 +- 19 files changed, 313 insertions(+), 2553 deletions(-) rename checker/src/test/java/org/checkerframework/checker/test/junit/{ResourceLeakCollections.java => ResourceLeakCollectionsTest.java} (81%) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index ef63869c98b7..d9b44db3b3c6 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -20,12 +20,7 @@ public class CollectionOwnershipAnalysis extends CFAbstractAnalysis { - /** - * Ignored-exception policy used by collection-ownership flow. - * - *

This policy matches the Resource Leak Checker policy except that exact {@link Throwable} - * exceptional edges are preserved. - */ + /** Ignored-exception policy used by collection-ownership flow. */ private final SetOfTypes ignoredExceptions; /** diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index cb6f9bbc5b9e..a549830bd3ba 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -81,12 +82,11 @@ public class CollectionOwnershipAnnotatedTypeFactory CollectionOwnershipAnalysis> { /** - * The {@code @} {@link MustCallAnnotatedTypeFactory} instance in the checker hierarchy. Used for - * getting the {@code @MustCall} type of expressions. + * The {@link MustCallAnnotatedTypeFactory} instance in the checker hierarchy. Used for getting + * the {@code @MustCall} type of expressions. */ private final MustCallAnnotatedTypeFactory mcAtf; - // TODO: review moved disposal-loop docs in this file. /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ private final IdentityHashMap conditionToDisposalLoopMap = new IdentityHashMap<>(); @@ -95,13 +95,18 @@ public class CollectionOwnershipAnnotatedTypeFactory private final IdentityHashMap conditionalBlockToDisposalLoopMap = new IdentityHashMap<>(); - /** Map from a {@link DisposalLoop} to the called-methods computed by MCCA for that loop. */ - private final IdentityHashMap> disposalLoopToMCCACalledMethodsMap = + /** + * Map from a {@link DisposalLoop} to the set of called-methods on the iterated element in its + * loop body. + */ + private final IdentityHashMap> disposalLoopToCalledMethodsMap = new IdentityHashMap<>(); - /** Map from a method to the disposal loops discovered for it before CM analysis. */ - private final IdentityHashMap> - preparedDisposalLoopsByMethod = new IdentityHashMap<>(); + /** + * Map from a {@code MethodTree} to the {@link DisposalLoop}s discovered in that method's body. + */ + private final IdentityHashMap> preparedDisposalLoopsByMethod = + new IdentityHashMap<>(); /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -212,13 +217,13 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { } /** - * Returns the called-methods computed by MCCA for a disposal loop. + * Returns the called-methods on the iterated element for a {@link DisposalLoop}. * - * @param disposalLoop the disposal loop - * @return the MCCA called-methods for {@code disposalLoop}, or {@code null} if none are populated + * @param disposalLoop the {@link DisposalLoop} + * @return the called-methods for {@link DisposalLoop}, or {@code null} if no information exits */ - public @Nullable Set getMccaCalledMethods(DisposalLoop disposalLoop) { - return disposalLoopToMCCACalledMethodsMap.get(disposalLoop); + public @Nullable Set getCalledMethods(DisposalLoop disposalLoop) { + return disposalLoopToCalledMethodsMap.get(disposalLoop); } /** @@ -234,47 +239,27 @@ public boolean isIgnoredExceptionType(TypeMirror exceptionType) { } /** - * Registers a disposal loop together with the called-methods computed by MCCA for it. + * Registers a {@link DisposalLoop} together with the called-methods on the iterated element over + * that loop's body. * - * @param disposalLoop the disposal loop - * @param MCCACalledMethods the called-methods computed by MCCA for the disposal loop + * @param disposalLoop the {@link DisposalLoop} + * @param calledMethods the called-methods on the iterated element over {@link DisposalLoop}'s + * body */ - private void registerMCCACalledMethods(DisposalLoop disposalLoop, Set MCCACalledMethods) { + private void registerCalledMethods(DisposalLoop disposalLoop, Set calledMethods) { conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); - disposalLoopToMCCACalledMethodsMap.put( - disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(MCCACalledMethods))); + disposalLoopToCalledMethodsMap.put( + disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(calledMethods))); } - // /** - // * Registers the MCCA called-methods for an RLCC-resolved disposal loop. - // * - // *

This bridge exists only until disposal-loop discovery and proof are fully coordinated by - // CO. - // * - // * @param resolvedLoop the RLCC-resolved loop whose MCCA analysis produced called-methods - // */ - // public void registerCalledMethodsForDisposalLoop( - // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop, Set mccaCalledMethods) { - // DisposalLoop disposalLoop = - // new DisposalLoop( - // resolvedLoop.collectionTree, - // resolvedLoop.collectionElementTree, - // resolvedLoop.collectionElementNode, - // resolvedLoop.condition, - // resolvedLoop.loopConditionalBlock, - // resolvedLoop.loopBodyEntryBlock, - // resolvedLoop.loopUpdateBlock); - // registerMCCACalledMethods(disposalLoop, mccaCalledMethods); - // } - /** - * Returns the disposal loops discovered in the given CFG. + * Scans a method CFG for {@link DisposalLoop}'s and returns the discovered loops. * * @param cfg the CFG to scan - * @return the disposal loops discovered in {@code cfg} + * @return the {@link DisposalLoop}'s discovered in {@code cfg} */ - private Set discoverDisposalLoops(ControlFlowGraph cfg) { + private Set scanForDisposalLoops(ControlFlowGraph cfg) { if (cfg.getUnderlyingAST().getKind() != UnderlyingAST.Kind.METHOD) { return Collections.emptySet(); } @@ -284,26 +269,29 @@ private Set discoverDisposalLoops(ControlFlowGraph cfg) { } /** - * Discovers and stores the disposal loops for a method CFG so RLCC transfer can seed its initial - * store before called-methods analysis runs. + * Discovers and stores the {@link DisposalLoop}'s for a method {@code cfg}. This discovery must + * happen before {@link + * org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsTransfer#initialStore(UnderlyingAST, + * List)} runs, so that the called-methods initial stores are populated for the loop's iterated + * temp elements, e.g., col.get(i), col.pop(). * - * @param cfg the method CFG whose disposal loops should be prepared + * @param cfg the method CFG whose disposal loops to be discovered */ - public void prepareDisposalLoops(ControlFlowGraph cfg) { + public void discoverDisposalLoops(ControlFlowGraph cfg) { MethodTree methodTree = getEnclosingMethodTree(cfg.getUnderlyingAST()); if (methodTree == null) { return; } - preparedDisposalLoopsByMethod.put(methodTree, new LinkedHashSet<>(discoverDisposalLoops(cfg))); + preparedDisposalLoopsByMethod.put(methodTree, new LinkedHashSet<>(scanForDisposalLoops(cfg))); } /** - * Returns the prepared disposal loops for the given underlying AST. + * Returns the {@link DisposalLoop}'s for the given underlying AST. * - * @param underlyingAST the underlying AST whose prepared loops should be returned - * @return the prepared disposal loops for {@code underlyingAST} + * @param underlyingAST the underlying AST whose disposal loops should be returned + * @return the set of disposal loops for {@code underlyingAST} */ - public Set getPreparedDisposalLoops(UnderlyingAST underlyingAST) { + public Set getDisposalLoops(UnderlyingAST underlyingAST) { MethodTree methodTree = getEnclosingMethodTree(underlyingAST); if (methodTree == null) { return Collections.emptySet(); @@ -316,10 +304,10 @@ public Set getPreparedDisposalLoops(UnderlyingAST underlyingAST) { } /** - * Removes and returns the prepared disposal loops for the given underlying AST. + * Removes and returns the {@link DisposalLoop}'s for the given underlying AST. * - * @param underlyingAST the underlying AST whose prepared loops should be removed - * @return the removed prepared disposal loops for {@code underlyingAST} + * @param underlyingAST the underlying AST whose disposal loops should be removed + * @return the removed disposal loops for {@code underlyingAST} */ private Set removePreparedDisposalLoops(UnderlyingAST underlyingAST) { MethodTree methodTree = getEnclosingMethodTree(underlyingAST); @@ -346,24 +334,10 @@ private Set removePreparedDisposalLoops(UnderlyingAST underlyingAS return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); } - /** - * Certifies prepared disposal loops for a method CFG before collection-ownership analysis runs. - * - * @param cfg the method CFG - * @param ast the CFG's underlying AST - */ @Override protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { - certifyPreparedDisposalLoops(cfg, ast); - } - - /** - * Runs MCCA on the prepared disposal loops for a method CFG and registers any certified loops. - * - * @param cfg the method CFG - * @param ast the CFG's underlying AST - */ - private void certifyPreparedDisposalLoops(ControlFlowGraph cfg, UnderlyingAST ast) { + // Discovers disposal loops in method's CFG, and for each disposal loop store the called-methods + // on the iterated element by the loop's body using MustCallConsistencyAnalyzer. if (ast.getKind() != UnderlyingAST.Kind.METHOD) { return; } @@ -376,10 +350,10 @@ private void certifyPreparedDisposalLoops(ControlFlowGraph cfg, UnderlyingAST as MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); for (DisposalLoop disposalLoop : preparedDisposalLoops) { - Set mccaCalledMethods = + Set calledMethods = mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoop); - if (mccaCalledMethods != null) { - registerMCCACalledMethods(disposalLoop, mccaCalledMethods); + if (calledMethods != null) { + registerCalledMethods(disposalLoop, calledMethods); } } } @@ -395,25 +369,6 @@ protected Set> createSupportedTypeQualifiers() { OwningCollectionBottom.class)); } - /** - * Fetches the store from the results of dataflow for {@code firstBlock}. If {@code - * afterFirstStore} is true, then the store after {@code firstBlock} is returned; if {@code - * afterFirstStore} is false, the store before {@code succBlock} is returned. - * - * @param afterFirstStore if true, use the store after the first block or the store before its - * successor, succBlock - * @param firstBlock a block - * @param succBlock {@code firstBlock}'s successor - * @return the appropriate CollectionOwnershipStore, populated with MustCall annotations, from the - * results of running dataflow - */ - public CollectionOwnershipStore getStoreForBlock( - boolean afterFirstStore, Block firstBlock, Block succBlock) { - return afterFirstStore - ? flowResult.getStoreAfter(firstBlock) - : flowResult.getStoreBefore(succBlock); - } - @Override protected ControlFlowGraph analyze( Queue> classQueue, @@ -437,26 +392,16 @@ protected ControlFlowGraph analyze( isStatic, capturedStore); if (cfg == null && ast.getKind() == UnderlyingAST.Kind.METHOD) { - // This uses the same RLLambda.java workaround pattern that originally lived in RLCC: + // This uses the same workaround pattern for lambdas that originally lived in + // RLCCalledMethodsAnnotatedTypeFactory#analyze: // run the resource-leak post-analysis immediately after the first method analysis, before - // containing lambdas are reanalyzed to fixpoint. CO owns that post-analysis now, so the - // workaround lives here. + // containing lambdas are reanalyzed to fixpoint. runResourceLeakPostAnalyze(result); preLambdaPostAnalyzedMethods.add(result); } return result; } - /** - * Performs post-analysis for the given CFG. - * - *

If resource-leak-specific post-analysis already ran during the first method analysis in - * {@link #analyze(Queue, Queue, UnderlyingAST, List, ControlFlowGraph, boolean, boolean, boolean, - * CollectionOwnershipStore)}, then this method avoids running it a second time for the enclosing - * method. - * - * @param cfg the CFG to post-analyze - */ @Override public void postAnalyze(ControlFlowGraph cfg) { if (!preLambdaPostAnalyzedMethods.remove(cfg)) { @@ -525,13 +470,10 @@ public boolean isOwningCollectionField(Element elt) { if (fieldType == null) { return false; } - switch (fieldType) { - case OwningCollection: - case OwningCollectionWithoutObligation: - return true; - default: - return false; - } + return switch (fieldType) { + case OwningCollection, OwningCollectionWithoutObligation -> true; + default -> false; + }; } } return false; @@ -545,9 +487,7 @@ public boolean isOwningCollectionField(Element elt) { */ public boolean isResourceCollectionField(Element elt) { if (elt.getKind().isField()) { - if (isResourceCollection(elt.asType())) { - return true; - } + return isResourceCollection(elt.asType()); } return false; } @@ -570,12 +510,7 @@ public boolean isOwningCollectionParameter(Element elt) { if (paramType == null) { return false; } - switch (paramType) { - case OwningCollection: - return true; - default: - return false; - } + return paramType == CollectionOwnershipType.OwningCollection; } } return false; @@ -609,7 +544,7 @@ public boolean isResourceCollection(Tree tree) { /** * Returns true if the given method is annotated {@code @CreatesCollectionObligation}. * - * @param methodElement a method + * @param methodElement the method * @return true if the method is annotated {@code @CreatesCollectionObligation} */ public boolean isCreatesCollectionObligationMethod(ExecutableElement methodElement) { @@ -620,7 +555,8 @@ public boolean isCreatesCollectionObligationMethod(ExecutableElement methodEleme * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} * call, or {@code null} if the call has no such argument. * - *

The current heuristic is that the inserted argument is the last argument at the call site. + *

The current heuristic is that the inserted element is the last argument at the call site. + * TODO: Make @CreatesCollectionObligation accept an index for the inserted elements position. * * @param tree a method invocation tree * @return the inserted argument tree, or null if there is none @@ -636,8 +572,6 @@ public boolean isCreatesCollectionObligationMethod(ExecutableElement methodEleme * Returns the argument whose obligation is transferred by a {@code @CreatesCollectionObligation} * call, or {@code null} if the call has no such argument. * - *

The current heuristic is that the inserted argument is the last argument at the call site. - * * @param node a method invocation node * @return the inserted argument node, or null if there is none */ @@ -688,9 +622,8 @@ public CollectionMutatorArgumentKind getCollectionMutatorArgumentKind(Tree inser // If the inserted element is a method call, and the callee has no @Owning annotation, then it's // definitely not owning. - if (insertedArgumentTree instanceof MethodInvocationTree) { - ExecutableElement callee = - TreeUtils.elementFromUse((MethodInvocationTree) insertedArgumentTree); + if (insertedArgumentTree instanceof MethodInvocationTree mit) { + ExecutableElement callee = TreeUtils.elementFromUse(mit); if (rlAtf.hasNotOwning(callee)) { return CollectionMutatorArgumentKind.DEFINITELY_NON_OWNING; } @@ -822,10 +755,10 @@ public CollectionOwnershipType getCoType(Node node, @Nullable CollectionOwnershi */ public CollectionOwnershipType getCoType(Tree tree) { JavaExpression jx = null; - if (tree instanceof ExpressionTree) { - jx = JavaExpression.fromTree((ExpressionTree) tree); - } else if (tree instanceof VariableTree) { - jx = JavaExpression.fromVariableTree((VariableTree) tree); + if (tree instanceof ExpressionTree expressionTree) { + jx = JavaExpression.fromTree(expressionTree); + } else if (tree instanceof VariableTree variableTree) { + jx = JavaExpression.fromVariableTree(variableTree); } try { CollectionOwnershipStore coStore = getStoreBefore(tree); @@ -889,7 +822,7 @@ public List getCollectionFieldDestructorAnnoFields(ExecutableElement met public boolean expressionIsFieldAccess(String e, VariableElement field) { try { JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); - return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); + return je instanceof FieldAccess fieldAccess && fieldAccess.getField().equals(field); } catch (JavaExpressionParseException ex) { // The parsing error will be reported elsewhere, assuming e was derived from an // annotation. @@ -908,9 +841,9 @@ public boolean expressionIsFieldAccess(String e, VariableElement field) { */ public JavaExpression stringToJavaExpression(String s, ExecutableElement method) { Tree methodTree = declarationFromElement(method); - if (methodTree instanceof MethodTree) { + if (methodTree instanceof MethodTree mit) { try { - return StringToJavaExpression.atMethodBody(s, (MethodTree) methodTree, checker); + return StringToJavaExpression.atMethodBody(s, mit, checker); } catch (JavaExpressionParseException ex) { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 7485685a0b26..98f5e1043ccc 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -84,8 +84,7 @@ public TransferResult visitAssignment( // In those cases the rhs keeps its existing ownership information. if (rhsType != null) { switch (rhsType) { - case OwningCollection: - case OwningCollectionWithoutObligation: + case OwningCollection, OwningCollectionWithoutObligation -> { JavaExpression rhsJE = JavaExpression.fromNode(rhs); if (node.isDesugaredFromEnhancedArrayForLoop() || atypeFactory.isOwningCollectionField( @@ -97,8 +96,8 @@ public TransferResult visitAssignment( hasExplicitNotOwningCollectionDeclaration(node) ? lhsJE : rhsJE, atypeFactory.NOTOWNINGCOLLECTION); } - break; - default: + } + default -> {} } } return res; @@ -120,7 +119,7 @@ private TransferResult updateStoreForDisposal CollectionOwnershipStore elseStore = res.getElseStore(); ExpressionTree collectionExpression = disposalLoop.expressionTree; JavaExpression collectionJE = JavaExpression.fromTree(collectionExpression); - Set disposalLoopCalledMethods = atypeFactory.getMccaCalledMethods(disposalLoop); + Set disposalLoopCalledMethods = atypeFactory.getCalledMethods(disposalLoop); CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionExpression); if (collectionCoType == CollectionOwnershipType.OwningCollection) { @@ -254,7 +253,7 @@ private TransferResult transferOwnershipForMe Element argElem = TreeUtils.elementFromTree(arg.getTree()); boolean transferOwnership = false; switch (paramType) { - case OwningCollection: + case OwningCollection -> { switch (argType) { case OwningCollection: case OwningCollectionWithoutObligation: @@ -262,16 +261,16 @@ private TransferResult transferOwnershipForMe break; default: } - break; - case OwningCollectionWithoutObligation: + } + case OwningCollectionWithoutObligation -> { switch (argType) { case OwningCollectionWithoutObligation: transferOwnership = true; break; default: } - break; - default: + } + default -> {} } if (transferOwnership) { if (argElem.getKind().isField()) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java index fbd44b0f2a16..959a82b65a87 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java @@ -5,7 +5,6 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; @@ -25,39 +24,36 @@ import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.IPair; -/** Utility methods shared by disposal-loop scanning and matcher resolution. */ +/** Utility methods shared by {@link DisposalLoop} scanning and AST matching. */ public final class CollectionOwnershipUtils { - private CollectionOwnershipUtils() {} - - /** - * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a - * lambda expression or a different method. - * - * @param methodTree the method currently being scanned, or {@code null} - * @return the enclosing method for the current loop, or {@code null} if it is not part of the - * scanned method - */ - static @Nullable MethodTree getEnclosingMethodForCollectionLoop(@Nullable MethodTree methodTree) { - return methodTree; + /** Do not instantiate */ + private CollectionOwnershipUtils() { + throw new BugInCF("CollectionOwnershipUtils is a utility class and should not be instantiated"); } /** - * Returns the statements in a loop body, regardless of whether the body is a block. + * Returns the given statement as a list of statements. + * + *

If {@code statement} is a {@link BlockTree}, returns that block's statements. Otherwise, + * returns a singleton list containing {@code statement}. Returns {@code null} if {@code + * statement} is {@code null}. * - * @param statement the loop body statement - * @return the loop body statements, or {@code null} if {@code statement} is {@code null} + * @param statement a loop-body statement + * @return a list of statements for {@code statement}, or {@code null} if {@code statement} is + * {@code null} */ - static @Nullable List getLoopBodyStatements( + static @Nullable List asStatementList( @Nullable StatementTree statement) { if (statement == null) { return null; } - return statement instanceof BlockTree - ? ((BlockTree) statement).getStatements() + return statement instanceof BlockTree blockTree + ? blockTree.getStatements() : Collections.singletonList(statement); } @@ -98,13 +94,14 @@ private CollectionOwnershipUtils() {} } /** - * Returns the tree to use as the loop-condition key. + * Returns the CFG-associated tree for the given tree, if one exists; otherwise returns the + * original tree. * * @param cfg the CFG containing the tree - * @param tree the original condition tree - * @return the CFG-associated tree for {@code tree}, if one exists; otherwise {@code tree} + * @param tree the original tree + * @return the CFG-associated tree for {@code tree}, or {@code tree} if no associated tree exists */ - static Tree treeForLoopCondition(ControlFlowGraph cfg, Tree tree) { + static Tree cfgAssociatedTreeFor(ControlFlowGraph cfg, Tree tree) { Node node = anyNodeForTree(cfg, tree); if (node != null && node.getTree() != null) { return node.getTree(); @@ -113,7 +110,7 @@ static Tree treeForLoopCondition(ControlFlowGraph cfg, Tree tree) { } /** - * Returns the simple name of the identifier referenced by the given expression, or {@code null} + * Returns the {@code Name} of the identifier referenced by the given expression, or {@code null} * if the expression does not reference an identifier. * * @param expr an expression @@ -124,9 +121,10 @@ static Name getNameFromExpressionTree(ExpressionTree expr) { return null; } switch (expr.getKind()) { - case IDENTIFIER: + case IDENTIFIER -> { return ((com.sun.source.tree.IdentifierTree) expr).getName(); - case MEMBER_SELECT: + } + case MEMBER_SELECT -> { MemberSelectTree mst = (MemberSelectTree) expr; Element elt = TreeUtils.elementFromUse(mst); if (elt.getKind() == ElementKind.FIELD) { @@ -136,15 +134,18 @@ static Name getNameFromExpressionTree(ExpressionTree expr) { } else { return null; } - case METHOD_INVOCATION: + } + case METHOD_INVOCATION -> { return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); - default: + } + default -> { return null; + } } } /** - * Returns the simple name of the identifier declared or referenced by the given statement, or + * Returns the {@code Name} of the identifier declared or referenced by the given statement, or * {@code null} if the statement does not declare or reference an identifier. * * @param expr the {@code StatementTree} @@ -155,27 +156,29 @@ static Name getNameFromStatementTree(StatementTree expr) { if (expr == null) { return null; } - switch (expr.getKind()) { - case VARIABLE: - return ((VariableTree) expr).getName(); - case EXPRESSION_STATEMENT: - return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); - default: - return null; - } + return switch (expr.getKind()) { + case VARIABLE -> ((VariableTree) expr).getName(); + case EXPRESSION_STATEMENT -> + getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); + default -> null; + }; } /** - * Returns the ExpressionTree of the collection in the given expression. + * Returns the expression that directly identifies the referenced value. * - * @param expr ExpressionTree - * @return the expression evaluates to or null if it doesn't + *

Identifiers and field accesses are returned unchanged. Method invocations are unwrapped to + * their receiver expression. Returns {@code null} for expressions that do not identify a value. + * + * @param expr an expression + * @return the expression that directly identifies the referenced value, or {@code null} */ - static ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { + static ExpressionTree baseExpression(ExpressionTree expr) { switch (expr.getKind()) { - case IDENTIFIER: + case IDENTIFIER -> { return expr; - case MEMBER_SELECT: + } + case MEMBER_SELECT -> { MemberSelectTree mst = (MemberSelectTree) expr; Element elt = TreeUtils.elementFromUse(mst); if (elt.getKind() == ElementKind.METHOD) { @@ -185,10 +188,13 @@ static ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { } else { return null; } - case METHOD_INVOCATION: - return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); - default: + } + case METHOD_INVOCATION -> { + return baseExpression(((MethodInvocationTree) expr).getMethodSelect()); + } + default -> { return null; + } } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java index c7649347db78..e776bdee5eab 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipVisitor.java @@ -63,12 +63,8 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { * Enforces the mutator policy for methods annotated {@code @CreatesCollectionObligation}. * *

A {@code @NotOwningCollection} receiver may only accept an inserted argument that is - * definitely non-owning at the call site. Owning receivers are allowed; the resource-leak - * analysis models any collection obligation they create. - * - *

This visitor only checks instance mutators on resource-collection receivers. The inserted - * argument is identified using the current {@code @CreatesCollectionObligation} heuristic in - * {@link CollectionOwnershipAnnotatedTypeFactory#getInsertedArgumentTree(MethodInvocationTree)}. + * definitely non-owning at the call site. Owning receivers are allowed; RLC models any collection + * obligation they create. * * @param tree the method invocation tree */ @@ -102,7 +98,7 @@ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) atypeFactory.getCollectionMutatorArgumentKind(insertedArgumentTree); String methodName = methodElement.getSimpleName().toString(); switch (receiverType) { - case NotOwningCollection: + case NotOwningCollection -> { if (insertedArgumentKind != CollectionOwnershipAnnotatedTypeFactory.CollectionMutatorArgumentKind .DEFINITELY_NON_OWNING) { @@ -112,10 +108,11 @@ private void enforceCreatesCollectionObligationPolicy(MethodInvocationTree tree) methodName, TreeUtils.toStringTruncated(insertedArgumentTree, 60)); } - break; - default: + } + default -> { // Owning receivers are handled by CO analysis; bottom and other cases are intentionally // ignored here. + } } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java index cda15afefe4e..9f64884ff310 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java @@ -7,7 +7,11 @@ import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.Node; -/** Stores the resolved CFG and AST facts for one disposal loop. */ +/** + * Stores the resolved CFG and AST facts for a disposal loop. A disposal loop is a loop that + * iterates over a resource collection and may call the disposal method, e.g., close() on the + * iterated resource. + */ public class DisposalLoop { /** The {@code ExpressionTree} for collection that this loop iterates over. */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java index 33fe767f79c3..62f2adaa5f7d 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java @@ -4,32 +4,26 @@ import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.WhileLoopTree; import com.sun.source.util.TreeScanner; import java.util.LinkedHashSet; import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.dataflow.cfg.ControlFlowGraph; -import org.checkerframework.dataflow.cfg.UnderlyingAST; -/** Scans one method tree and discovers disposal loops in its CFG. */ +/** Scans one method tree and discovers {@link DisposalLoop}'s in its CFG. */ public class DisposalLoopScanner extends TreeScanner { /** The CO type factory used for collection-ownership queries. */ - private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + private final CollectionOwnershipAnnotatedTypeFactory coAtf; - /** The RLCC type factory used for declaration lookup and ignored-exception checks. */ + /** The RLCC type factory used for declaration lookup. */ private final RLCCalledMethodsAnnotatedTypeFactory rlccAtf; /** The CFG of the method currently being scanned. */ private final ControlFlowGraph cfg; - /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ - private final @Nullable MethodTree methodTree; - /** Disposal loops discovered while scanning the current method tree. */ private final Set disposalLoops = new LinkedHashSet<>(); @@ -45,29 +39,22 @@ public class DisposalLoopScanner extends TreeScanner { /** * Creates a scanner for disposal loops in one method CFG. * - * @param atypeFactory the CO type factory + * @param coAtf the CO type factory * @param rlccAtf the RLCC type factory * @param cfg the CFG to scan */ public DisposalLoopScanner( - CollectionOwnershipAnnotatedTypeFactory atypeFactory, + CollectionOwnershipAnnotatedTypeFactory coAtf, RLCCalledMethodsAnnotatedTypeFactory rlccAtf, ControlFlowGraph cfg) { - this.atypeFactory = atypeFactory; + this.coAtf = coAtf; this.rlccAtf = rlccAtf; this.cfg = cfg; - UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); - if (underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { - this.methodTree = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); - } else { - this.methodTree = null; - } - this.indexedForDisposalLoopMatcher = - new IndexedForDisposalLoopMatcher(this.atypeFactory, this.cfg, this.methodTree); + this.indexedForDisposalLoopMatcher = new IndexedForDisposalLoopMatcher(this.coAtf, this.cfg); this.whileDisposalLoopMatcher = - new WhileDisposalLoopMatcher(this.atypeFactory, this.rlccAtf, this.cfg, this.methodTree); + new WhileDisposalLoopMatcher(this.coAtf, this.rlccAtf, this.cfg); this.enhancedForDisposalLoopResolver = - new EnhancedForDisposalLoopResolver(this.atypeFactory, this.cfg, this.methodTree); + new EnhancedForDisposalLoopResolver(this.coAtf, this.cfg); } /** @@ -126,8 +113,7 @@ public Void visitForLoop(ForLoopTree tree, Void p) { } /** - * Matches while-loops that may fulfill collection obligations and resolves their CFG-local loop - * facts. + * Matches a {@link DisposalLoop} that uses while-loops and resolves their CFG-local loop facts. * * @param tree the while-loop to inspect * @param p the scan parameter diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java index 2cb52e92b82d..d09f199aa35b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -3,7 +3,6 @@ import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import java.util.ArrayDeque; import java.util.Deque; @@ -23,59 +22,47 @@ import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.IPair; -/** Resolves enhanced-`for` loops that may discharge collection obligations. */ +/** Resolves enhanced-`for` {@link DisposalLoop} from CFG. */ final class EnhancedForDisposalLoopResolver { /** The CO type factory used for collection-ownership queries. */ - private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + private final CollectionOwnershipAnnotatedTypeFactory coAtf; /** The CFG of the method currently being scanned. */ private final ControlFlowGraph cfg; - /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ - private final @Nullable MethodTree methodTree; - /** * Creates a resolver for enhanced-`for` disposal loops. * - * @param atypeFactory the CO type factory + * @param coAtf the CO type factory * @param cfg the CFG being scanned - * @param methodTree the enclosing method, or {@code null} */ EnhancedForDisposalLoopResolver( - CollectionOwnershipAnnotatedTypeFactory atypeFactory, - ControlFlowGraph cfg, - @Nullable MethodTree methodTree) { - this.atypeFactory = atypeFactory; + CollectionOwnershipAnnotatedTypeFactory coAtf, ControlFlowGraph cfg) { + this.coAtf = coAtf; this.cfg = cfg; - this.methodTree = methodTree; } /** - * Adds an enhanced-for-loop that fulfills collection obligations. + * Returns the {@link DisposalLoop} along with its facts if the enhanced-for-loop iterates over a + * resource collection. * * @param tree the enhanced-for-loop to inspect * @return the matched disposal loop, or {@code null} if the loop does not match */ @Nullable DisposalLoop match(EnhancedForLoopTree tree) { - MethodTree enclosingMethodTree = - CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); - if (enclosingMethodTree == null) { - return null; - } - ExpressionTree collectionTree = - CollectionOwnershipUtils.collectionTreeFromExpression(tree.getExpression()); + ExpressionTree collectionTree = CollectionOwnershipUtils.baseExpression(tree.getExpression()); if (collectionTree == null) { return null; } - if (!atypeFactory.isResourceCollection(collectionTree)) { + if (!coAtf.isResourceCollection(collectionTree)) { return null; } return resolveEnhancedForLoop(tree); } /** - * Resolves an enhanced-for-loop candidate into a CFG-resolved loop. + * Resolves an enhanced-for-loop candidate into a {@link DisposalLoop}. * * @param tree the enhanced-for-loop to resolve * @return the CFG-resolved loop, or {@code null} if it cannot be resolved @@ -91,8 +78,8 @@ final class EnhancedForDisposalLoopResolver { Block currentBlock = worklist.removeFirst(); for (Node node : currentBlock.getNodes()) { - if (node instanceof MethodInvocationNode) { - DisposalLoop resolvedLoop = resolveEnhancedForLoop((MethodInvocationNode) node, tree); + if (node instanceof MethodInvocationNode methodInvocationNode) { + DisposalLoop resolvedLoop = resolveEnhancedForLoop(methodInvocationNode, tree); if (resolvedLoop != null) { return resolvedLoop; } @@ -100,8 +87,7 @@ final class EnhancedForDisposalLoopResolver { } for (IPair successorAndExceptionType : - CollectionOwnershipUtils.getSuccessorsExceptIgnoredExceptions( - currentBlock, atypeFactory)) { + CollectionOwnershipUtils.getSuccessorsExceptIgnoredExceptions(currentBlock, coAtf)) { Block successorBlock = successorAndExceptionType.first; if (successorBlock != null && visitedBlocks.add(successorBlock)) { worklist.addLast(successorBlock); @@ -114,7 +100,7 @@ final class EnhancedForDisposalLoopResolver { /** * Returns a resolved collection loop if the given node is desugared from an enhanced-for-loop - * over a collection. + * over a resource collection. * * @param methodInvocationNode the node to check * @param tree the enhanced-for-loop being resolved @@ -189,10 +175,11 @@ final class EnhancedForDisposalLoopResolver { + blockContainingLoopCondition.getSuccessors().size() + " successors instead of 1."); } - Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); - if (!(conditionalBlock instanceof ConditionalBlock)) { + Block maybeConditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); + if (!(maybeConditionalBlock instanceof ConditionalBlock conditionalBlock)) { throw new BugInCF( - "loop condition successor is not ConditionalBlock, but: " + conditionalBlock.getClass()); + "loop condition successor is not ConditionalBlock, but: " + + maybeConditionalBlock.getClass()); } if (loopVariableNode == null || loopVariableNode.getTree() == null || node.getTree() == null) { return null; @@ -203,7 +190,7 @@ final class EnhancedForDisposalLoopResolver { loopVariableNode.getTree(), loopVariableNode, node.getTree(), - (ConditionalBlock) conditionalBlock, + conditionalBlock, loopBodyEntryBlock, loopUpdateBlock); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index 1629cfc2ca51..2508379d4946 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -10,7 +10,6 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; @@ -29,60 +28,48 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches indexed `for` loops that may discharge collection obligations. */ +/** Matches indexed `for` {@link DisposalLoop}'s that iterates over a resource collection. */ final class IndexedForDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ - private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + private final CollectionOwnershipAnnotatedTypeFactory coAtf; /** The CFG of the method currently being scanned. */ private final ControlFlowGraph cfg; - /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ - private final @Nullable MethodTree methodTree; - /** * Creates a matcher for indexed `for` disposal loops. * - * @param atypeFactory the CO type factory + * @param coAtf the CO type factory * @param cfg the CFG being scanned - * @param methodTree the enclosing method, or {@code null} */ IndexedForDisposalLoopMatcher( - CollectionOwnershipAnnotatedTypeFactory atypeFactory, - ControlFlowGraph cfg, - @Nullable MethodTree methodTree) { - this.atypeFactory = atypeFactory; + CollectionOwnershipAnnotatedTypeFactory coAtf, ControlFlowGraph cfg) { + this.coAtf = coAtf; this.cfg = cfg; - this.methodTree = methodTree; } /** - * Marks the for-loop if it potentially fulfills collection obligations of a collection. + * Returns the {@link DisposalLoop} if the `for` loop iterates over a resource collection and + * follows a specific loop shape described in {@link #nameOfCollectionThatAllElementsAreCalledOn}. * * @param tree a `for` loop with exactly one loop variable * @return the matched disposal loop, or {@code null} if the loop does not match */ @Nullable DisposalLoop match(ForLoopTree tree) { - MethodTree enclosingMethodTree = - CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); - if (enclosingMethodTree == null) { - return null; - } - List loopBodyStatements = - CollectionOwnershipUtils.getLoopBodyStatements(tree.getStatement()); + CollectionOwnershipUtils.asStatementList(tree.getStatement()); if (loopBodyStatements == null) { return null; } StatementTree init = tree.getInitializer().get(0); ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); ExpressionStatementTree update = tree.getUpdate().get(0); - if (!(condition instanceof BinaryTree)) { + if (!(condition instanceof BinaryTree binaryTreeCondition)) { return null; } Name identifierInHeader = - nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); + nameOfCollectionThatAllElementsAreCalledOn(init, binaryTreeCondition, update); Name iterator = CollectionOwnershipUtils.getNameFromStatementTree(init); if (identifierInHeader == null || iterator == null) { return null; @@ -101,10 +88,10 @@ final class IndexedForDisposalLoopMatcher { Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); return new DisposalLoop( - CollectionOwnershipUtils.collectionTreeFromExpression(collectionElementTree), + CollectionOwnershipUtils.baseExpression(collectionElementTree), collectionElementTree, nodeForCollectionElt, - CollectionOwnershipUtils.treeForLoopCondition(cfg, condition), + CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, condition), (ConditionalBlock) conditionalBlock, loopBodyEntryBlock, loopUpdateBlock); @@ -141,9 +128,8 @@ Name nameOfCollectionThatAllElementsAreCalledOn( if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { UnaryTree inc = (UnaryTree) update.getExpression(); - if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) - return null; - VariableTree initVar = (VariableTree) init; + if (!(init instanceof VariableTree initVar) + || !(inc.getExpression() instanceof IdentifierTree)) return null; if (!(initVar.getInitializer() instanceof LiteralTree) || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { @@ -166,10 +152,9 @@ Name nameOfCollectionThatAllElementsAreCalledOn( && TreeUtils.isSizeAccess(condition.getRightOperand())) { ExpressionTree methodSelect = ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); - if (methodSelect instanceof MemberSelectTree) { - MemberSelectTree mst = (MemberSelectTree) methodSelect; + if (methodSelect instanceof MemberSelectTree mst) { Element elt = TreeUtils.elementFromTree(mst.getExpression()); - if (ResourceLeakUtils.isCollection(elt, atypeFactory)) { + if (ResourceLeakUtils.isCollection(elt, coAtf)) { return CollectionOwnershipUtils.getNameFromExpressionTree(mst.getExpression()); } } @@ -201,17 +186,13 @@ Name nameOfCollectionThatAllElementsAreCalledOn( @Override public Void visitUnary(UnaryTree tree, Void p) { switch (tree.getKind()) { - case PREFIX_DECREMENT: - case POSTFIX_DECREMENT: - case PREFIX_INCREMENT: - case POSTFIX_INCREMENT: + case PREFIX_DECREMENT, POSTFIX_DECREMENT, PREFIX_INCREMENT, POSTFIX_INCREMENT -> { if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getExpression()) == iterator) { blockIsIllegal.set(true); } - break; - default: - break; + } + default -> {} } return super.visitUnary(tree, p); } @@ -268,16 +249,14 @@ private boolean isIthCollectionElement(Tree tree, Name index) { if (tree == null || index == null) { return false; } - if (tree instanceof MethodInvocationTree + if (tree instanceof MethodInvocationTree mit && index == CollectionOwnershipUtils.getNameFromExpressionTree( TreeUtils.getIdxForGetCall(tree))) { - MethodInvocationTree mit = (MethodInvocationTree) tree; ExpressionTree methodSelect = mit.getMethodSelect(); - if (methodSelect instanceof MemberSelectTree) { - MemberSelectTree mst = (MemberSelectTree) methodSelect; + if (methodSelect instanceof MemberSelectTree mst) { Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); - return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); + return ResourceLeakUtils.isCollection(receiverElt, coAtf); } } return false; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index efd8cec5c641..85f73542f405 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -4,9 +4,9 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; @@ -38,41 +38,35 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches `while` loops that may discharge collection obligations. */ +/** Matches `while` {@link DisposalLoop} that iterates over a resource collection. */ final class WhileDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ - private final CollectionOwnershipAnnotatedTypeFactory atypeFactory; + private final CollectionOwnershipAnnotatedTypeFactory coAtf; - /** The RLCC type factory used for declaration lookup and ignored-exception checks. */ + /** The RLCC type factory used for declaration lookup. */ private final RLCCalledMethodsAnnotatedTypeFactory rlccAtf; /** The CFG of the method currently being scanned. */ private final ControlFlowGraph cfg; - /** The method currently being scanned, or {@code null} if the CFG is not for a method. */ - private final @Nullable MethodTree methodTree; - /** Lazily-computed CFG facts for while-loop resolution. */ private @Nullable WhileLoopResolutionCache whileLoopCache; /** * Creates a matcher for `while` disposal loops. * - * @param atypeFactory the CO type factory + * @param coAtf the CO type factory * @param rlccAtf the RLCC type factory * @param cfg the CFG being scanned - * @param methodTree the enclosing method, or {@code null} */ WhileDisposalLoopMatcher( - CollectionOwnershipAnnotatedTypeFactory atypeFactory, + CollectionOwnershipAnnotatedTypeFactory coAtf, RLCCalledMethodsAnnotatedTypeFactory rlccAtf, - ControlFlowGraph cfg, - @Nullable MethodTree methodTree) { - this.atypeFactory = atypeFactory; + ControlFlowGraph cfg) { + this.coAtf = coAtf; this.rlccAtf = rlccAtf; this.cfg = cfg; - this.methodTree = methodTree; } /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ @@ -257,7 +251,7 @@ private static Set naturalLoop( } /** - * Description of an accepted while-loop header form. + * Description of a supported while disposal loop header form. * *

Each header form determines which extraction methods are allowed in the loop body. */ @@ -297,7 +291,7 @@ private static final class WhileSpec { * match when present. */ private static final class WhileHeaderMatch { - /** Owning collection expression whose element obligations may be discharged. */ + /** Collection expression whose element obligations may be discharged. */ final ExpressionTree collectionTree; /** Collection variable name whose writes should invalidate the match, if one exists. */ @@ -350,7 +344,7 @@ private static final class BodyExtraction { } /** - * Matches a while-loop that may fulfill collection obligations. + * Matches a {@link DisposalLoop} that uses a while-loop and resolves its CFG-local loop facts. * *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, @@ -360,18 +354,13 @@ private static final class BodyExtraction { * @return the matched disposal loop, or {@code null} if the loop does not match */ @Nullable DisposalLoop match(WhileLoopTree tree) { - MethodTree enclosingMethodTree = - CollectionOwnershipUtils.getEnclosingMethodForCollectionLoop(methodTree); - if (enclosingMethodTree == null) { - return null; - } ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); WhileHeaderMatch header = matchWhileHeader(condNoParens); if (header == null) { return null; } List bodyStatements = - CollectionOwnershipUtils.getLoopBodyStatements(tree.getStatement()); + CollectionOwnershipUtils.asStatementList(tree.getStatement()); if (bodyStatements == null) { return null; } @@ -413,7 +402,7 @@ private static final class BodyExtraction { header.collectionTree, extraction.extractionCall, elementNode, - CollectionOwnershipUtils.treeForLoopCondition(cfg, condNoParens), + CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, condNoParens), cblock, loopBodyEntryBlock, loopUpdateBlock); @@ -431,8 +420,7 @@ private static final class BodyExtraction { * @return the recovered header facts, or {@code null} if the header is unsupported */ private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { - if (cond instanceof MethodInvocationTree) { - MethodInvocationTree mit = (MethodInvocationTree) cond; + if (cond instanceof MethodInvocationTree mit) { if (TreeUtils.isHasNextCall(mit)) { ExpressionTree recv = receiverOfInvocation(mit); Name itName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); @@ -448,16 +436,16 @@ private static final class BodyExtraction { } } - if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { - ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); + if (cond instanceof UnaryTree unaryTreeCond && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { + ExpressionTree inner = TreeUtils.withoutParens(unaryTreeCond.getExpression()); WhileHeaderMatch m = matchNonEmptyFromExpr(inner); if (m != null) { return m; } } - if (cond instanceof BinaryTree) { - WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); + if (cond instanceof BinaryTree binaryTreeCond) { + WhileHeaderMatch m = matchNonEmptyFromSize(binaryTreeCond); if (m != null) { return m; } @@ -473,10 +461,9 @@ private static final class BodyExtraction { * @return the recovered header facts, or {@code null} if the expression does not match */ private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { - if (!(inner instanceof MethodInvocationTree)) { + if (!(inner instanceof MethodInvocationTree mit)) { return null; } - MethodInvocationTree mit = (MethodInvocationTree) inner; if (!isIsEmptyCall(mit)) { return null; } @@ -489,10 +476,10 @@ private static final class BodyExtraction { return null; } Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + if (!ResourceLeakUtils.isCollection(recvElt, coAtf)) { return null; } - ExpressionTree colTree = CollectionOwnershipUtils.collectionTreeFromExpression(recv); + ExpressionTree colTree = CollectionOwnershipUtils.baseExpression(recv); if (colTree == null) { return null; } @@ -516,19 +503,17 @@ private static final class BodyExtraction { ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); MethodInvocationTree sizeCall = null; - com.sun.source.tree.LiteralTree zero = null; + LiteralTree zero = null; if (k == Tree.Kind.GREATER_THAN) { - if (left instanceof MethodInvocationTree - && right instanceof com.sun.source.tree.LiteralTree) { - sizeCall = (MethodInvocationTree) left; - zero = (com.sun.source.tree.LiteralTree) right; + if (left instanceof MethodInvocationTree mitLeft && right instanceof LiteralTree ltRight) { + sizeCall = mitLeft; + zero = ltRight; } } else { - if (left instanceof com.sun.source.tree.LiteralTree - && right instanceof MethodInvocationTree) { - zero = (com.sun.source.tree.LiteralTree) left; - sizeCall = (MethodInvocationTree) right; + if (left instanceof LiteralTree ltLeft && right instanceof MethodInvocationTree ltRight) { + zero = ltLeft; + sizeCall = ltRight; } } @@ -552,11 +537,11 @@ private static final class BodyExtraction { } Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { + if (!ResourceLeakUtils.isCollection(recvElt, coAtf)) { return null; } - ExpressionTree colTree = CollectionOwnershipUtils.collectionTreeFromExpression(recv); + ExpressionTree colTree = CollectionOwnershipUtils.baseExpression(recv); if (colTree == null) { return null; } @@ -572,10 +557,9 @@ private static final class BodyExtraction { */ private boolean isIsEmptyCall(MethodInvocationTree invocation) { ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { + if (!(sel instanceof MemberSelectTree ms)) { return false; } - MemberSelectTree ms = (MemberSelectTree) sel; return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); } @@ -587,8 +571,8 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { */ private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { ExpressionTree sel = invocation.getMethodSelect(); - if (sel instanceof MemberSelectTree) { - return ((MemberSelectTree) sel).getExpression(); + if (sel instanceof MemberSelectTree memberSelectTree) { + return memberSelectTree.getExpression(); } return null; } @@ -618,33 +602,31 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { } Tree decl = rlccAtf.declarationFromElement(itElt); - if (!(decl instanceof VariableTree)) { + if (!(decl instanceof VariableTree variableTreeDecl)) { return null; } - ExpressionTree init = ((VariableTree) decl).getInitializer(); - if (!(init instanceof MethodInvocationTree)) { + ExpressionTree init = variableTreeDecl.getInitializer(); + if (!(init instanceof MethodInvocationTree initCall)) { return null; } - MethodInvocationTree initCall = (MethodInvocationTree) init; ExpressionTree sel = initCall.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { + if (!(sel instanceof MemberSelectTree ms)) { return null; } - MemberSelectTree ms = (MemberSelectTree) sel; if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { return null; } ExpressionTree colExpr = ms.getExpression(); Element colElt = TreeUtils.elementFromTree(colExpr); - if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { + if (!ResourceLeakUtils.isCollection(colElt, coAtf)) { return null; } - return CollectionOwnershipUtils.collectionTreeFromExpression(colExpr); + return CollectionOwnershipUtils.baseExpression(colExpr); } /** @@ -684,11 +666,10 @@ private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { private void recordExtractionIfAny(ExpressionTree expr) { expr = TreeUtils.withoutParens(expr); - if (!(expr instanceof MethodInvocationTree)) { + if (!(expr instanceof MethodInvocationTree mit)) { return; } - MethodInvocationTree mit = (MethodInvocationTree) expr; if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { return; } @@ -726,8 +707,8 @@ public Void visitVariable(VariableTree vt, Void p) { @Override public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { ExpressionTree sel = mit.getMethodSelect(); - if (sel instanceof MemberSelectTree) { - ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); + if (sel instanceof MemberSelectTree memberSelect) { + ExpressionTree recv = memberSelect.getExpression(); recordExtractionIfAny(recv); } return super.visitMethodInvocation(mit, p); @@ -757,10 +738,9 @@ public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { private boolean isExtractionCallOnHeaderVar( MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree)) { + if (!(sel instanceof MemberSelectTree ms)) { return false; } - MemberSelectTree ms = (MemberSelectTree) sel; String methodName = ms.getIdentifier().toString(); if (!allowedExtractMethods.contains(methodName)) { return false; @@ -780,14 +760,14 @@ private boolean isExtractionCallOnHeaderVar( */ private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { for (Block succ : block.getSuccessors()) { - if (succ instanceof ConditionalBlock) { - return (ConditionalBlock) succ; + if (succ instanceof ConditionalBlock conditionalBlockSucc) { + return conditionalBlockSucc; } } - if (block instanceof SingleSuccessorBlock) { - Block succ = ((SingleSuccessorBlock) block).getSuccessor(); - if (succ instanceof ConditionalBlock) { - return (ConditionalBlock) succ; + if (block instanceof SingleSuccessorBlock singleSuccessorBlock) { + Block succ = singleSuccessorBlock.getSuccessor(); + if (succ instanceof ConditionalBlock conditionalBlockSucc) { + return conditionalBlockSucc; } } return null; diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index 51bc377b5c62..7dc54d590a70 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -154,119 +154,6 @@ public MustCallAnnotatedTypeFactory(BaseTypeChecker checker) { this.postInit(); } - // /** - // * Records a potentially fulfilling collection loop for the given enclosing method. - // * - // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - // * this method does nothing. - // * - // * @param enclosingMethodTree the method containing the loop - // * @param collectionTree the collection iterated over by the loop - // * @param collectionElementTree the tree for the collection element - // * @param conditionTree the loop condition - // * @param loopBodyEntryBlock the CFG block for the loop body entry - // * @param loopConditionalBlock the CFG conditional block for the loop - // * @param collectionElementNode the CFG node for the iterated element - // */ - // public void recordPotentiallyFulfillingCollectionLoop( - // MethodTree enclosingMethodTree, - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree conditionTree, - // Block loopBodyEntryBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionElementNode) { - // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - // if (rlccAtf == null) { - // return; - // } - // rlccAtf.recordPotentiallyFulfillingCollectionLoop( - // enclosingMethodTree, - // collectionTree, - // collectionElementTree, - // conditionTree, - // loopBodyEntryBlock, - // loopConditionalBlock, - // collectionElementNode); - // } - // - // /** - // * Records a potentially fulfilling enhanced-for-loop for the given enclosing method. - // * - // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - // * this method does nothing. - // * - // * @param enclosingMethodTree the method containing the loop - // * @param enhancedForLoopTree the enhanced-for-loop tree - // */ - // public void recordPotentiallyFulfillingEnhancedForLoop( - // MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { - // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - // if (rlccAtf == null) { - // return; - // } - // rlccAtf.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, - // enhancedForLoopTree); - // } - // - // /** - // * Records a CFG-resolved potentially fulfilling collection loop for the given enclosing - // method. - // * - // *

If this Must Call factory is not running under the Resource Leak Checker hierarchy, then - // * this method does nothing. - // * - // * @param enclosingMethodTree the method containing the loop - // * @param collectionTree the collection iterated over by the loop - // * @param collectionElementTree the tree for the collection element - // * @param conditionTree the loop condition - // * @param loopBodyEntryBlock the CFG block for the loop body entry - // * @param loopUpdateBlock the CFG block for the loop update - // * @param loopConditionalBlock the CFG conditional block for the loop - // * @param collectionElementNode the CFG node for the iterated element - // */ - // public void recordResolvedPotentiallyFulfillingCollectionLoop( - // MethodTree enclosingMethodTree, - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree conditionTree, - // Block loopBodyEntryBlock, - // Block loopUpdateBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionElementNode) { - // RLCCalledMethodsAnnotatedTypeFactory rlccAtf = getRlccAtfIfPartOfHierarchy(); - // if (rlccAtf == null) { - // return; - // } - // rlccAtf.recordResolvedPotentiallyFulfillingCollectionLoop( - // enclosingMethodTree, - // collectionTree, - // collectionElementTree, - // conditionTree, - // loopBodyEntryBlock, - // loopUpdateBlock, - // loopConditionalBlock, - // collectionElementNode); - // } - - // /** - // * Returns the RLCC called-methods type factory if this factory is part of the Resource Leak - // * Checker hierarchy, or {@code null} otherwise. - // * - // * @return the RLCC called-methods type factory, or {@code null} - // */ - // private @Nullable RLCCalledMethodsAnnotatedTypeFactory getRlccAtfIfPartOfHierarchy() { - // SourceChecker currentChecker = checker; - // while (currentChecker != null) { - // String currentCheckerName = currentChecker.getClass().getCanonicalName(); - // if (ResourceLeakUtils.rlcCheckers.contains(currentCheckerName)) { - // return ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this); - // } - // currentChecker = currentChecker.getParentChecker(); - // } - // return null; - // } - @Override public void setRoot(@Nullable CompilationUnitTree newRoot) { super.setRoot(newRoot); @@ -293,8 +180,8 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar continue; } if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) { - if (tree != null && tree instanceof NewClassTree) { - if (((NewClassTree) tree).getTypeArguments().isEmpty()) { + if (tree instanceof NewClassTree newClassTree) { + if (newClassTree.getTypeArguments().isEmpty()) { // Diamond [new Class()<>]. Not explicit generic type param. // This will be inferred later. Don't put it to bottom here. continue; @@ -318,8 +205,8 @@ private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclar if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) { typeArg.replaceAnnotation(BOTTOM); } - } else if (typeArg instanceof AnnotatedTypeVariable) { - AnnotatedTypeMirror upperBound = ((AnnotatedTypeVariable) typeArg).getUpperBound(); + } else if (typeArg instanceof AnnotatedTypeVariable annotatedTypeVariable) { + AnnotatedTypeMirror upperBound = annotatedTypeVariable.getUpperBound(); // set back to bottom if the type var is a captured wildcard // or if it doesn't have a manual MustCallUnknown anno if (typeArg.containsCapturedTypes() diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java index f27ba56c9e94..e3ce2bd774a5 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -329,1074 +329,4 @@ protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { public Void visitAnnotation(AnnotationTree tree, Void p) { return null; } - - // /** - // * Syntactically matches indexed for-loops that iterate over all elements of a collection. - // * - // *

This logic lives in the Must Call visitor because matching must complete before - // collection - // * ownership transfer runs. - // */ - // @Override - // public Void visitForLoop(ForLoopTree tree, Void p) { - // boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == - // 1; - // if (singleLoopVariable) { - // detectCollectionObligationFulfillingLoop(tree); - // } - // return super.visitForLoop(tree, p); - // } - // - //// /** - //// * Performs AST-only matching for while-loops that may fulfill collection obligations. - //// * - //// *

RLCC resolves the remaining CFG-local loop facts later, during post-analysis of the - //// * enclosing method. - //// */ - //// @Override - //// public Void visitWhileLoop(WhileLoopTree tree, Void p) { - //// detectCollectionObligationFulfillingWhileLoop(tree); - //// return super.visitWhileLoop(tree, p); - //// } - // - // /** - // * Performs AST-only matching for enhanced-for-loops that may fulfill collection obligations. - // * - // *

The visitor records only the loop tree here. RLCC resolves the desugared iterator CFG - // shape - // * later during post-analysis of the enclosing method. - // * - // * @param tree the enhanced-for-loop to inspect - // */ - // @Override - // public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { - // detectPotentiallyFulfillingEnhancedForLoop(tree); - // return super.visitEnhancedForLoop(tree, p); - // } - - // /** - // * Records an enhanced-for-loop that potentially fulfills collection obligations. - // * - // *

This method only performs AST matching. RLCC resolves the CFG-specific loop facts later, - // * during post-analysis of the enclosing method. - // * - // * @param tree the enhanced-for-loop to inspect - // */ - // private void detectPotentiallyFulfillingEnhancedForLoop(EnhancedForLoopTree tree) { - // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - // if (enclosingMethodTree == null) { - // return; - // } - // ExpressionTree collectionTree = collectionTreeFromExpression(tree.getExpression()); - // if (collectionTree == null) { - // return; - // } - // if (!ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(atypeFactory) - // .isResourceCollection(collectionTree)) { - // return; - // } - // atypeFactory.recordPotentiallyFulfillingEnhancedForLoop(enclosingMethodTree, tree); - // } - - // /** - // * Description of an accepted while-loop header form. - // * - // *

Each header form determines which extraction methods are allowed in the loop body. - // */ - // private static final class WhileSpec { - // /** Methods that may extract an element when this header form is used. */ - // final Set extractMethods; - // - // /** - // * Creates a while-loop header specification. - // * - // * @param extractMethods methods that may extract an element from the looped collection - // */ - // WhileSpec(Set extractMethods) { - // this.extractMethods = extractMethods; - // } - // } - // - // /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ - // private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); - // - // /** - // * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... - // ... - // * }}, including {@code size() > 0} and {@code 0 < size()} variants. - // */ - // private static final WhileSpec NONEMPTY_SPEC = - // new WhileSpec( - // new HashSet<>( - // Arrays.asList( - // // Queue/Deque (null-returning) - // "poll", - // "pollFirst", - // "pollLast", - // // Deque (throws on empty, but guarded by condition) - // "remove", - // "removeFirst", - // "removeLast", - // // Stack - // "pop"))); - // - // /** - // * AST facts recovered from a matched while-loop header. - // * - // *

{@link #collectionTree} is the collection whose element obligations may be discharged. - // * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link - // * #collectionVarNameForBailout} names the collection variable whose writes should invalidate - // the - // * match when present. - // */ - // private static final class WhileHeaderMatch { - // /** Owning collection expression whose element obligations may be discharged. */ - // final ExpressionTree collectionTree; - // - // /** Collection variable name whose writes should invalidate the match, if one exists. */ - // final @Nullable Name collectionVarNameForBailout; - // - // /** Iterator variable or collection variable constrained by the loop header. */ - // final Name headerVar; - // - // /** Accepted extraction shape for the matched loop header. */ - // final WhileSpec spec; - // - // /** - // * Creates a summary of the AST facts recovered from a matched while-loop header. - // * - // * @param collectionTree the owning collection expression to mark - // * @param collectionVarNameForBailout collection variable whose writes invalidate the match - // * @param headerVar iterator or collection variable constrained by the header - // * @param spec accepted extraction shape for the matched loop header - // */ - // WhileHeaderMatch( - // ExpressionTree collectionTree, - // @Nullable Name collectionVarNameForBailout, - // Name headerVar, - // WhileSpec spec) { - // this.collectionTree = collectionTree; - // this.collectionVarNameForBailout = collectionVarNameForBailout; - // this.headerVar = headerVar; - // this.spec = spec; - // } - // } - // - // /** - // * Records a while-loop that may fulfill collection obligations. - // * - // *

This method performs AST matching plus the small amount of CFG lookup needed to identify - // the - // * condition block, the conditional successor, the body entry block, and the extracted element - // * node. RLCC resolves the remaining CFG-local fact, the loop update block, later during - // * post-analysis. - // * - // *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and - // * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > - // 0)}, - // * and {@code while (0 < q.size())}. - // * - // * @param tree the while-loop to inspect - // */ - // private void detectCollectionObligationFulfillingWhileLoop(WhileLoopTree tree) { - // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - // if (enclosingMethodTree == null) { - // return; - // } - // // 1) Match header - // ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); - // WhileHeaderMatch header = matchWhileHeader(condNoParens); - // if (header == null) { - // return; - // } - // // 2) Extract body statements. - // List bodyStatements = getLoopBodyStatements(tree.getStatement()); - // if (bodyStatements == null) { - // return; - // } - // // 3) Find exactly one extraction call in the body to be sound. - // BodyExtraction extraction = - // findSingleExtractionInWhileBody( - // bodyStatements, - // header.headerVar, - // header.collectionVarNameForBailout, - // header.spec.extractMethods); - // if (extraction == null) { - // return; - // } - // // Resolve CFG-local metadata (except loopUpdateBlock). - // Block condBlock = firstBlockForTree(condNoParens); - // if (condBlock == null) { - // return; - // } - // // Find the ConditionalBlock that branches on the while condition. - // ConditionalBlock cblock = findConditionalSuccessor(condBlock); - // if (cblock == null) { - // // condition often lives in ExceptionBlocks; try walking up preds and retry - // Block peeled = peelExceptionBlocksToPred(condBlock); - // if (peeled != null) { - // cblock = findConditionalSuccessor(peeled); - // } - // } - // if (cblock == null) { - // return; - // } - // - // Block loopBodyEntryBlock = cblock.getThenSuccessor(); - // - // // Node for the extraction call tree (it.next()/q.poll()/s.pop()). - // Node elementNode = anyNodeForTree(extraction.extractionCall); - // if (elementNode == null) { - // return; - // } - // - // // 4) Record a potentially fulfilling collection loop. - // // - // // Store: - // // - collectionTree (resources / q / s) - // // - collectionElementTree (it.next() / q.poll() / s.pop()) - // // - condition tree (the while condition) - // atypeFactory.recordPotentiallyFulfillingCollectionLoop( - // enclosingMethodTree, - // header.collectionTree, - // extraction.extractionCall, - // condNoParens, - // loopBodyEntryBlock, - // cblock, - // elementNode); - // } - // - // /** - // * Returns the enclosing method for the current loop, or {@code null} if the loop is inside a - // * lambda expression. - // * - // *

The per-method loop-state refactor records only loops that are part of the enclosing - // method - // * analysis. Lambda-local loop support can be added separately if needed. - // * - // * @return the enclosing method for the current loop, or {@code null} if it is inside a lambda - // */ - // private @Nullable MethodTree getEnclosingMethodForCollectionLoop() { - // Tree enclosingMethodOrLambda = TreePathUtil.enclosingMethodOrLambda(getCurrentPath()); - // if (enclosingMethodOrLambda instanceof MethodTree) { - // return (MethodTree) enclosingMethodOrLambda; - // } - // return null; - // } - // - // /** - // * Returns the statements in a loop body, regardless of whether the body is a block. - // * - // * @param statement the loop body statement - // * @return the loop body statements, or {@code null} if {@code statement} is {@code null} - // */ - // private @Nullable List getLoopBodyStatements( - // @Nullable StatementTree statement) { - // if (statement == null) { - // return null; - // } - // return statement instanceof BlockTree - // ? ((BlockTree) statement).getStatements() - // : Collections.singletonList(statement); - // } - // - // /** - // * Returns the first CFG block associated with the given tree. - // * - // * @param tree a tree - // * @return the first CFG block associated with {@code tree}, or {@code null} if none is known - // */ - // private @Nullable Block firstBlockForTree(Tree tree) { - // Set nodes = atypeFactory.getNodesForTree(tree); - // if (nodes == null || nodes.isEmpty()) { - // return null; - // } - // for (Node n : nodes) { - // Block block = n.getBlock(); - // if (block != null) { - // return block; - // } - // } - // return null; - // } - // - // /** - // * Returns an arbitrary CFG node associated with the given tree. - // * - // * @param tree a tree - // * @return a CFG node associated with {@code tree}, or {@code null} if none is known - // */ - // private @Nullable Node anyNodeForTree(Tree tree) { - // Set nodes = atypeFactory.getNodesForTree(tree); - // if (nodes == null || nodes.isEmpty()) { - // return null; - // } - // return nodes.iterator().next(); - // } - // - // /** - // * Returns the conditional successor reached from the given block, if one is immediately - // visible. - // * - // * @param block a CFG block - // * @return the conditional successor of {@code block}, or {@code null} if none is found - // */ - // private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { - // for (Block succ : block.getSuccessors()) { - // if (succ instanceof ConditionalBlock) { - // return (ConditionalBlock) succ; - // } - // } - // if (block instanceof SingleSuccessorBlock) { - // Block succ = ((SingleSuccessorBlock) block).getSuccessor(); - // if (succ instanceof ConditionalBlock) { - // return (ConditionalBlock) succ; - // } - // } - // return null; - // } - // - // /** - // * Walks backward through exception blocks to recover the predecessor block that leads to the - // * actual loop conditional. - // * - // *

This is needed because loop conditions such as {@code iterator.hasNext()} may be - // represented - // * by exception blocks before reaching the conditional branch. - // * - // * @param block a CFG block - // * @return a predecessor block to retry from, or {@code null} if no such block is found - // */ - // private @Nullable Block peelExceptionBlocksToPred(Block block) { - // Block cur = block; - // Set visitedBlocks = new HashSet<>(); - // while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { - // Set preds = cur.getPredecessors(); - // if (preds.size() != 1) { - // break; - // } - // Block p = preds.iterator().next(); - // if (p == null) { - // break; - // } - // cur = p; - // } - // return cur; - // } - // - // /** - // * Matches supported while-loop header forms and returns the recovered loop facts. - // * - // *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code - // * while (c.size() > 0)}, and {@code while (0 < c.size())}. - // * - // * @param cond the while-loop condition with parentheses removed - // * @return the recovered header facts, or {@code null} if the header is unsupported - // */ - // private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { - // // Case A: while (it.hasNext()) - // if (cond instanceof MethodInvocationTree) { - // MethodInvocationTree mit = (MethodInvocationTree) cond; - // if (TreeUtils.isHasNextCall(mit)) { - // ExpressionTree recv = receiverOfInvocation(mit); - // Name itName = getNameFromExpressionTree(recv); - // if (itName == null) { - // return null; - // } - // ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); - // if (colExpr == null) { - // return null; - // } - // Name colName = getNameFromExpressionTree(colExpr); - // return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); - // } - // } - // - // // Case B1: while (!c.isEmpty()) - // if (cond instanceof UnaryTree && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { - // ExpressionTree inner = TreeUtils.withoutParens(((UnaryTree) cond).getExpression()); - // WhileHeaderMatch m = matchNonEmptyFromExpr(inner); - // if (m != null) { - // return m; - // } - // } - // - // // Case B2: while (c.size() > 0) or while (0 < c.size()) - // if (cond instanceof BinaryTree) { - // WhileHeaderMatch m = matchNonEmptyFromSize((BinaryTree) cond); - // if (m != null) { - // return m; - // } - // } - // - // return null; - // } - // - // /** - // * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. - // * - // * @param inner the expression under the logical complement - // * @return the recovered header facts, or {@code null} if the expression does not match - // */ - // private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { - // if (!(inner instanceof MethodInvocationTree)) { - // return null; - // } - // MethodInvocationTree mit = (MethodInvocationTree) inner; - // if (!isIsEmptyCall(mit)) { - // return null; - // } - // ExpressionTree recv = receiverOfInvocation(mit); - // if (recv == null) { - // return null; - // } - // Name varName = getNameFromExpressionTree(recv); - // if (varName == null) { - // return null; - // } - // Element recvElt = TreeUtils.elementFromTree(recv); - // if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { - // return null; - // } - // ExpressionTree colTree = collectionTreeFromExpression(recv); - // if (colTree == null) { - // return null; - // } - // return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); - // } - // - // /** - // * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < - // * c.size()}. - // * - // * @param condition the binary condition - // * @return the recovered header facts, or {@code null} if the expression does not match - // */ - // private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { - // Tree.Kind k = condition.getKind(); - // if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { - // return null; - // } - // - // ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); - // ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); - // - // // Normalize: accept "c.size() > 0" or "0 < c.size()" - // MethodInvocationTree sizeCall = null; - // LiteralTree zero = null; - // - // if (k == Tree.Kind.GREATER_THAN) { - // // left must be size(), right must be 0 - // if (left instanceof MethodInvocationTree && right instanceof LiteralTree) { - // sizeCall = (MethodInvocationTree) left; - // zero = (LiteralTree) right; - // } - // } else { // LESS_THAN - // // left must be 0, right must be size() - // if (left instanceof LiteralTree && right instanceof MethodInvocationTree) { - // zero = (LiteralTree) left; - // sizeCall = (MethodInvocationTree) right; - // } - // } - // - // if (sizeCall == null - // || !(zero.getValue() instanceof Integer) - // || (Integer) zero.getValue() != 0) { - // return null; - // } - // if (!TreeUtils.isSizeAccess(sizeCall)) { - // return null; - // } - // - // ExpressionTree recv = receiverOfInvocation(sizeCall); - // if (recv == null) { - // return null; - // } - // - // Name varName = getNameFromExpressionTree(recv); - // if (varName == null) { - // return null; - // } - // - // Element recvElt = TreeUtils.elementFromTree(recv); - // if (!ResourceLeakUtils.isCollection(recvElt, atypeFactory)) { - // return null; - // } - // - // ExpressionTree colTree = collectionTreeFromExpression(recv); - // if (colTree == null) { - // return null; - // } - // - // return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); - // } - // - // /** - // * Returns whether the given invocation is an {@code isEmpty()} call with no arguments. - // * - // * @param invocation a method invocation - // * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments - // */ - // private boolean isIsEmptyCall(MethodInvocationTree invocation) { - // ExpressionTree sel = invocation.getMethodSelect(); - // if (!(sel instanceof MemberSelectTree)) { - // return false; - // } - // MemberSelectTree ms = (MemberSelectTree) sel; - // return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); - // } - // - // /** - // * Returns the explicit receiver of the given invocation, if present. - // * - // * @param invocation a method invocation - // * @return the explicit receiver, or {@code null} if none exists - // */ - // private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { - // ExpressionTree sel = invocation.getMethodSelect(); - // if (sel instanceof MemberSelectTree) { - // return ((MemberSelectTree) sel).getExpression(); - // } - // return null; - // } - // - // /** - // * Recovers the collection expression from an iterator receiver in a header such as {@code - // while - // * (it.hasNext())}. - // * - // *

This only recognizes local iterator variables initialized by {@code col.iterator()}. - // * - // * @param iteratorExpr the iterator receiver expression - // * @return the collection expression, or {@code null} if it cannot be recovered - // */ - // private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( - // ExpressionTree iteratorExpr) { - // if (iteratorExpr == null) { - // return null; - // } - // - // Element itElt = TreeUtils.elementFromTree(iteratorExpr); - // if (!(itElt instanceof VariableElement)) { - // return null; - // } - // - // // Only recover from local variable declaration with initializer "col.iterator()" - // if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { - // return null; - // } - // - // Tree decl = atypeFactory.declarationFromElement(itElt); - // if (!(decl instanceof VariableTree)) { - // return null; - // } - // - // ExpressionTree init = ((VariableTree) decl).getInitializer(); - // if (!(init instanceof MethodInvocationTree)) { - // return null; - // } - // - // MethodInvocationTree initCall = (MethodInvocationTree) init; - // ExpressionTree sel = initCall.getMethodSelect(); - // if (!(sel instanceof MemberSelectTree)) { - // return null; - // } - // - // MemberSelectTree ms = (MemberSelectTree) sel; - // if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { - // return null; - // } - // - // ExpressionTree colExpr = ms.getExpression(); - // Element colElt = TreeUtils.elementFromTree(colExpr); - // if (!ResourceLeakUtils.isCollection(colElt, atypeFactory)) { - // return null; - // } - // - // return collectionTreeFromExpression(colExpr); - // } - // - // /** - // * One extracted element use recovered from a while-loop body. - // * - // *

The extraction call is the expression that removes or advances to the next element, such - // as - // * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. - // */ - // private static final class BodyExtraction { - // /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ - // final MethodInvocationTree extractionCall; - // - // /** - // * Creates a body extraction summary. - // * - // * @param extractionCall extraction call found in the loop body - // */ - // BodyExtraction(MethodInvocationTree extractionCall) { - // this.extractionCall = extractionCall; - // } - // } - // - // /** - // * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code - // * null}. - // * - // *

This matcher rejects writes to the iterator/header variable and, when present, to the - // * collection variable itself, because such writes invalidate the header/body correspondence - // used - // * by later CFG verification. - // * - // * @param statements the loop body statements - // * @param headerVar the iterator or collection variable constrained by the header - // * @param collectionVarName the collection variable to protect from writes, if any - // * @param allowedExtractMethods the extraction methods allowed by the matched header - // * @return the unique extraction in the loop body, or {@code null} if the body is unsupported - // */ - // private @Nullable BodyExtraction findSingleExtractionInWhileBody( - // List statements, - // Name headerVar, - // @Nullable Name collectionVarName, - // Set allowedExtractMethods) { - // - // AtomicBoolean illegal = new AtomicBoolean(false); - // final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; - // final int[] extractionCount = new int[] {0}; - // - // TreeScanner scanner = - // new TreeScanner() { - // - // private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { - // Name assigned = getNameFromExpressionTree(lhs); - // if (assigned != null) { - // if (assigned == headerVar) illegal.set(true); - // if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); - // } - // } - // - // private void recordExtractionIfAny(ExpressionTree expr) { - // expr = TreeUtils.withoutParens(expr); - // if (!(expr instanceof MethodInvocationTree)) { - // return; - // } - // - // MethodInvocationTree mit = (MethodInvocationTree) expr; - // if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { - // return; - // } - // - // extractionCount[0]++; - // if (extractionCount[0] > 1) { - // illegal.set(true); - // return; - // } - // extraction[0] = mit; - // } - // - // @Override - // public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - // markWriteIfTargetsHeaderOrCollection(node.getVariable()); - // return super.visitCompoundAssignment(node, p); - // } - // - // @Override - // public Void visitAssignment(AssignmentTree node, Void p) { - // markWriteIfTargetsHeaderOrCollection(node.getVariable()); - // recordExtractionIfAny(node.getExpression()); // r = it.next() - // return super.visitAssignment(node, p); - // } - // - // @Override - // public Void visitVariable(VariableTree vt, Void p) { - // ExpressionTree init = vt.getInitializer(); - // if (init != null) { - // recordExtractionIfAny(init); // T r = it.next() - // } - // return super.visitVariable(vt, p); - // } - // - // @Override - // public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - // // Direct use: it.next().close() => receiver is it.next() - // ExpressionTree sel = mit.getMethodSelect(); - // if (sel instanceof MemberSelectTree) { - // ExpressionTree recv = ((MemberSelectTree) sel).getExpression(); - // recordExtractionIfAny(recv); - // } - // return super.visitMethodInvocation(mit, p); - // } - // }; - // - // for (StatementTree st : statements) { - // scanner.scan(st, null); - // if (illegal.get()) break; - // } - // - // if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { - // return null; - // } - // return new BodyExtraction(extraction[0]); - // } - // - // /** - // * Returns whether the given invocation is an allowed extraction call on the matched header - // * variable. - // * - // * @param invocation a method invocation - // * @param headerVar the iterator or collection variable constrained by the header - // * @param allowedExtractMethods extraction methods permitted by the matched header form - // * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} - // */ - // private boolean isExtractionCallOnHeaderVar( - // MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { - // ExpressionTree sel = invocation.getMethodSelect(); - // if (!(sel instanceof MemberSelectTree)) { - // return false; - // } - // MemberSelectTree ms = (MemberSelectTree) sel; - // String methodName = ms.getIdentifier().toString(); - // if (!allowedExtractMethods.contains(methodName)) { - // return false; - // } - // if (!invocation.getArguments().isEmpty()) { - // return false; - // } - // Name recv = getNameFromExpressionTree(ms.getExpression()); - // return recv != null && recv == headerVar; - // } - // - // /** - // * Marks the for-loop if it potentially fulfills collection obligations of a collection. - // * - // * @param tree a `for` loop with exactly one loop variable - // */ - // private void detectCollectionObligationFulfillingLoop(ForLoopTree tree) { - // MethodTree enclosingMethodTree = getEnclosingMethodForCollectionLoop(); - // if (enclosingMethodTree == null) { - // return; - // } - // - // List loopBodyStatements = - // getLoopBodyStatements(tree.getStatement()); - // if (loopBodyStatements == null) { - // return; - // } - // StatementTree init = tree.getInitializer().get(0); - // ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); - // ExpressionStatementTree update = tree.getUpdate().get(0); - // if (!(condition instanceof BinaryTree)) { - // return; - // } - // Name identifierInHeader = - // nameOfCollectionThatAllElementsAreCalledOn(init, (BinaryTree) condition, update); - // Name iterator = getNameFromStatementTree(init); - // if (identifierInHeader == null || iterator == null) { - // return; - // } - // ExpressionTree collectionElementTree = - // getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); - // if (collectionElementTree != null) { - // // Pattern match succeeded, now mark the loop in the respective datastructures. - // - // Block loopConditionBlock = null; - // for (Node node : atypeFactory.getNodesForTree(condition)) { - // Block blockOfNode = node.getBlock(); - // if (blockOfNode != null) { - // loopConditionBlock = blockOfNode; - // break; - // } - // } - // - // Block loopUpdateBlock = null; - // for (Node node : atypeFactory.getNodesForTree(update.getExpression())) { - // Block blockOfNode = node.getBlock(); - // if (blockOfNode != null) { - // loopUpdateBlock = blockOfNode; - // break; - // } - // } - // - // Set collectionEltNodes = atypeFactory.getNodesForTree(collectionElementTree); - // Node nodeForCollectionElt = null; - // if (collectionEltNodes != null) { - // nodeForCollectionElt = collectionEltNodes.iterator().next(); - // } - // if (loopUpdateBlock == null || loopConditionBlock == null) { - // return; - // } - // // Record the loop in the RLCCalledMethods ATF's per-method loop state so that it can - // // analyze it later. - // // MustCallConsistencyAnalyzer.analyzeResolvedPotentiallyFulfillingCollectionLoop will - // then - // // add verified fulfilling loops to the collection-ownership ATF. - // Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); - // Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); - // atypeFactory.recordResolvedPotentiallyFulfillingCollectionLoop( - // enclosingMethodTree, - // collectionTreeFromExpression(collectionElementTree), - // collectionElementTree, - // tree.getCondition(), - // loopBodyEntryBlock, - // loopUpdateBlock, - // (ConditionalBlock) conditionalBlock, - // nodeForCollectionElt); - // } - // } - // - // /** - // * Conservatively decides whether a loop iterates over all elements of some collection, using - // the - // * following rules: - // * - // *

    - // *
  • only one loop variable - // *
  • initialization must be of the form i = 0 - // *
  • condition must be of the form (i < col.size()) - // *
  • update must be prefix or postfix {@code ++} - // *
- // * - // * Returns: - // * - // *
    - // *
  • null, if any of the above rules is violated - // *
  • the name of the collection if the loop condition is of the form (i < col.size()) - // *
- // * - // * @param init the initializer of the loop - // * @param condition the loop condition - // * @param update the loop update - // * @return the name of the collection that the loop iterates over all elements of, or null - // */ - // protected Name nameOfCollectionThatAllElementsAreCalledOn( - // StatementTree init, BinaryTree condition, ExpressionStatementTree update) { - // Tree.Kind updateKind = update.getExpression().getKind(); - // if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { - // UnaryTree inc = (UnaryTree) update.getExpression(); - // - // // Verify update is of form i++ or ++i and init is variable initializer. - // if (!(init instanceof VariableTree) || !(inc.getExpression() instanceof IdentifierTree)) - // return null; - // VariableTree initVar = (VariableTree) init; - // - // // Verify that intializer is i=0. - // if (!(initVar.getInitializer() instanceof LiteralTree) - // || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { - // return null; - // } - // - // // Verify that condition is of the form: i < something. - // if (!(condition.getLeftOperand() instanceof IdentifierTree)) { - // return null; - // } - // - // // Verify that i=0, i statements, Name identifierInHeader, Name iterator) { - // AtomicBoolean blockIsIllegal = new AtomicBoolean(false); - // final ExpressionTree[] collectionElementTree = {null}; - // - // TreeScanner scanner = - // new TreeScanner() { - // @Override - // public Void visitUnary(UnaryTree tree, Void p) { - // switch (tree.getKind()) { - // case PREFIX_DECREMENT: - // case POSTFIX_DECREMENT: - // case PREFIX_INCREMENT: - // case POSTFIX_INCREMENT: - // if (getNameFromExpressionTree(tree.getExpression()) == iterator) { - // blockIsIllegal.set(true); - // } - // break; - // default: - // break; - // } - // return super.visitUnary(tree, p); - // } - // - // @Override - // public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { - // if (getNameFromExpressionTree(tree.getVariable()) == iterator) { - // blockIsIllegal.set(true); - // } - // return super.visitCompoundAssignment(tree, p); - // } - // - // @Override - // public Void visitAssignment(AssignmentTree tree, Void p) { - // Name assignedVariable = getNameFromExpressionTree(tree.getVariable()); - // if (assignedVariable == iterator || assignedVariable == identifierInHeader) { - // blockIsIllegal.set(true); - // } - // - // return super.visitAssignment(tree, p); - // } - // - // // check whether corresponds to collection.get(i) - // @Override - // public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - // if (isIthCollectionElement(mit, iterator) - // && identifierInHeader == getNameFromExpressionTree(mit) - // && identifierInHeader != null) { - // collectionElementTree[0] = mit; - // } - // return super.visitMethodInvocation(mit, p); - // } - // }; - // - // for (StatementTree stmt : statements) { - // scanner.scan(stmt, null); - // } - // if (!blockIsIllegal.get() && collectionElementTree[0] != null) { - // return collectionElementTree[0]; - // } - // return null; - // } - // - // /** - // * Returns the simple name of the identifier referenced by the given expression, or {@code - // null} - // * if the expression does not reference an identifier. - // * - // * @param expr an expression - // * @return the name of the referenced identifier, or {@code null} if none - // */ - // protected Name getNameFromExpressionTree(ExpressionTree expr) { - // if (expr == null) { - // return null; - // } - // switch (expr.getKind()) { - // case IDENTIFIER: - // return ((IdentifierTree) expr).getName(); - // case MEMBER_SELECT: - // MemberSelectTree mst = (MemberSelectTree) expr; - // Element elt = TreeUtils.elementFromUse(mst); - // if (elt.getKind() == ElementKind.FIELD) { - // // this.files ==> "files" (NOT "this") - // return mst.getIdentifier(); - // } else if (elt.getKind() == ElementKind.METHOD) { - // // resources.size ==> "resources" - // return getNameFromExpressionTree(mst.getExpression()); - // } else { - // return null; - // } - // case METHOD_INVOCATION: - // return getNameFromExpressionTree(((MethodInvocationTree) expr).getMethodSelect()); - // default: - // return null; - // } - // } - // - // /** - // * Returns the simple name of the identifier declared or referenced by the given statement, or - // * {@code null} if the statement does not declare or reference an identifier. - // * - // * @param expr the {@code StatementTree} - // * @return the name of the identifier declared or referenced by the statement, or {@code null} - // if - // * none - // */ - // protected Name getNameFromStatementTree(StatementTree expr) { - // if (expr == null) { - // return null; - // } - // switch (expr.getKind()) { - // case VARIABLE: - // return ((VariableTree) expr).getName(); - // case EXPRESSION_STATEMENT: - // return getNameFromExpressionTree(((ExpressionStatementTree) expr).getExpression()); - // default: - // return null; - // } - // } - // - // /** - // * Returns the ExpressionTree of the collection in the given expression. - // * - // * @param expr ExpressionTree - // * @return the expression evaluates to or null if it doesn't - // */ - // protected ExpressionTree collectionTreeFromExpression(ExpressionTree expr) { - // switch (expr.getKind()) { - // case IDENTIFIER: - // return expr; - // case MEMBER_SELECT: - // MemberSelectTree mst = (MemberSelectTree) expr; - // Element elt = TreeUtils.elementFromUse(mst); - // if (elt.getKind() == ElementKind.METHOD) { - // return ((MemberSelectTree) expr).getExpression(); - // } else if (elt.getKind() == ElementKind.FIELD) { - // return expr; - // } else { - // return null; - // } - // case METHOD_INVOCATION: - // return collectionTreeFromExpression(((MethodInvocationTree) expr).getMethodSelect()); - // default: - // return null; - // } - // } - // - // /** - // * Returns true if the given tree is of the form collection.get(i), where i is the given index - // * name. - // * - // * @param tree the tree to check - // * @param index the index variable name - // * @return true if the given tree is of the form collection.get(index) - // */ - // private boolean isIthCollectionElement(Tree tree, Name index) { - // if (tree == null || index == null) { - // return false; - // } - // if (tree instanceof MethodInvocationTree - // && index == getNameFromExpressionTree(TreeUtils.getIdxForGetCall(tree))) { - // MethodInvocationTree mit = (MethodInvocationTree) tree; - // ExpressionTree methodSelect = mit.getMethodSelect(); - // if (methodSelect instanceof MemberSelectTree) { - // MemberSelectTree mst = (MemberSelectTree) methodSelect; - // Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); - // return ResourceLeakUtils.isCollection(receiverElt, atypeFactory); - // } - // } - // return false; - // } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4e01e509c1a4..4d4d449e9354 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -329,12 +329,12 @@ public Obligation getReplacement( public static Obligation fromTree(Tree tree) { JavaExpression jx; Element elem; - if (tree instanceof ExpressionTree) { - jx = JavaExpression.fromTree((ExpressionTree) tree); - elem = TreeUtils.elementFromTree((ExpressionTree) tree); - } else if (tree instanceof VariableTree) { - jx = JavaExpression.fromVariableTree((VariableTree) tree); - elem = TreeUtils.elementFromDeclaration((VariableTree) tree); + if (tree instanceof ExpressionTree expressionTree) { + jx = JavaExpression.fromTree(expressionTree); + elem = TreeUtils.elementFromTree(expressionTree); + } else if (tree instanceof VariableTree variableTree) { + jx = JavaExpression.fromVariableTree(variableTree); + elem = TreeUtils.elementFromDeclaration(variableTree); } else { throw new IllegalArgumentException( "Tree must be ExpressionTree or VariableTree but is " + tree.getClass()); @@ -402,9 +402,9 @@ private boolean canBeSatisfiedThrough(LocalVariableNode localVariableNode) { private boolean canBeSatisfiedThrough(Tree tree) { for (ResourceAlias alias : resourceAliases) { if (alias.tree.equals(tree) - || ((tree instanceof ExpressionTree) - && JavaExpression.fromTree((ExpressionTree) tree) != null - && alias.reference.equals(JavaExpression.fromTree((ExpressionTree) tree)))) { + || ((tree instanceof ExpressionTree expressionTree) + && JavaExpression.fromTree(expressionTree) != null + && alias.reference.equals(JavaExpression.fromTree(expressionTree)))) { return true; } } @@ -599,11 +599,10 @@ public boolean equals(@Nullable Object obj) { if (obj == this) { return true; } - if (!(obj instanceof CollectionObligation)) { + if (!(obj instanceof CollectionObligation collectionObligation)) { return false; } - return super.equals(obj) - && ((CollectionObligation) obj).mustCallMethod.equals(this.mustCallMethod); + return super.equals(obj) && collectionObligation.mustCallMethod.equals(this.mustCallMethod); } } @@ -950,9 +949,7 @@ private void addObligationsForCreatesCollectionObligationAnno( throw new BugInCF("Method receiver not in collection ownership store: " + receiverNode); } switch (receiverType) { - case OwningCollectionWithoutObligation: - // fall through - case OwningCollection: + case OwningCollectionWithoutObligation, OwningCollection -> { if (!receiverIsOwningField) { List mustCallValues = coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree()); @@ -974,8 +971,8 @@ private void addObligationsForCreatesCollectionObligationAnno( } // Transfer the inserted element's obligation to the owning collection receiver. consumeInsertedArgumentObligationIfSingleElementInsert(obligations, node); - break; - default: + } + default -> {} } } } @@ -1085,8 +1082,8 @@ private void updateObligationsForInvocation( addObligationsForOwningCollectionReturn(obligations, node); - if (node instanceof MethodInvocationNode) { - addObligationsForCreatesCollectionObligationAnno(obligations, (MethodInvocationNode) node); + if (node instanceof MethodInvocationNode methodInvocationNode) { + addObligationsForCreatesCollectionObligationAnno(obligations, methodInvocationNode); } if (!shouldTrackInvocationResult(obligations, node, false)) { @@ -1715,10 +1712,9 @@ private void handleAssignmentToCollectionElementVariable( break; } for (ResourceAlias alias : o.resourceAliases) { - if ((alias.tree instanceof ExpressionTree) - && (rhsExpr.getTree() instanceof ExpressionTree) - && TreeUtils.sameTree( - (ExpressionTree) alias.tree, (ExpressionTree) rhsExpr.getTree())) { + if ((alias.tree instanceof ExpressionTree aliasExprTree) + && (rhsExpr.getTree() instanceof ExpressionTree rhsExprTree) + && TreeUtils.sameTree(aliasExprTree, rhsExprTree)) { Set newResourceAliasesForObligation = new LinkedHashSet<>(o.resourceAliases); // It is possible to observe assignments to temporary variables, e.g., @@ -2110,10 +2106,12 @@ private void checkReassignmentToOwningCollectionField( } switch (lhsCoType) { - case NotOwningCollection: + case NotOwningCollection -> { // doesn't own elements. safe to overwrite. return; - case OwningCollectionWithoutObligation: + // doesn't own elements. safe to overwrite. + } + case OwningCollectionWithoutObligation -> { // no obligation. assignment allowed. // but if rhs is owning, demand CreatesMustCallFor("this") if (rhsCoType == CollectionOwnershipType.OwningCollection @@ -2127,7 +2125,8 @@ private void checkReassignmentToOwningCollectionField( } } return; - case OwningCollection: + } + case OwningCollection -> { // assignment not allowed checker.reportError( node.getTree(), @@ -2136,7 +2135,8 @@ private void checkReassignmentToOwningCollectionField( lhs.getTree(), "Field assignment might overwrite field's current value"); return; - default: + } + default -> {} } } } @@ -2431,10 +2431,8 @@ private String receiverAsString(FieldAccessNode fieldAccessNode) { if (receiver instanceof SuperNode) { return "super"; } - if (receiver instanceof FieldAccessNode) { - return receiverAsString((FieldAccessNode) receiver) - + "." - + ((FieldAccessNode) receiver).getFieldName(); + if (receiver instanceof FieldAccessNode fieldAccessReceiver) { + return receiverAsString(fieldAccessReceiver) + "." + fieldAccessReceiver.getFieldName(); } throw new TypeSystemError( "unexpected receiver of field assignment: " + receiver + " of type " + receiver.getClass()); @@ -2662,8 +2660,8 @@ private void propagateObligationsToSuccessorBlocks( updateObligationsForOwningReturn(obligations, cfg, rn); } else if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { updateObligationsForInvocation(obligations, node, successorAndExceptionType.second); - } else if (node instanceof FieldAccessNode) { - checkOwningResourceCollectionFieldAccess((FieldAccessNode) node); + } else if (node instanceof FieldAccessNode fieldAccessNode) { + checkOwningResourceCollectionFieldAccess(fieldAccessNode); } // All other types of nodes are ignored. This is safe, because other kinds of // nodes cannot create or modify the resource-alias sets that the algorithm is @@ -2757,7 +2755,7 @@ private void propagateObligationsToSuccessorBlock( if ((currentBlock instanceof ConditionalBlock conditionalBlock) && disposalLoop != null) { if (conditionalBlock.getElseSuccessor().equals(successor)) { isElseEdgeOfDisposalLoop = true; - disposalLoopCalledMethods = coAtf.getMccaCalledMethods(disposalLoop); + disposalLoopCalledMethods = coAtf.getCalledMethods(disposalLoop); } } @@ -2767,8 +2765,8 @@ private void propagateObligationsToSuccessorBlock( for (Obligation obligation : obligations) { if (isElseEdgeOfDisposalLoop && disposalLoopCalledMethods != null) { - if (obligation instanceof CollectionObligation) { - String mustCallMethodOfCo = ((CollectionObligation) obligation).mustCallMethod; + if (obligation instanceof CollectionObligation collectionObligation) { + String mustCallMethodOfCo = collectionObligation.mustCallMethod; if (disposalLoopCalledMethods.contains(mustCallMethodOfCo)) { // Don't propagate this obligation along this edge, as the called-methods for this // disposal loop already fulfills it. @@ -3159,12 +3157,12 @@ private void checkMustCall( return; } - if (obligation instanceof CollectionObligation) { + if (obligation instanceof CollectionObligation collectionObligation) { ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); if (!reportedErrorAliases.contains(firstAlias)) { if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { reportedErrorAliases.add(firstAlias); - String methodName = ((CollectionObligation) obligation).mustCallMethod; + String methodName = collectionObligation.mustCallMethod; checker.reportError( firstAlias.tree, "unfulfilled.collection.obligations", @@ -3466,11 +3464,8 @@ public static String collectionToString(Collection bwos) { } /** - * Analyze the loop body of a {@link DisposalLoop} to compute the definitely called-methods on - * every iterated element on every path. - * - *

This method should be called after the called-method-analysis is finished (in the {@code - * postAnalyze(cfg)} method of the {@code RLCCalledMethodsAnnotatedTypeFactory}). + * Analyze the loop body of a {@link DisposalLoop} to compute the definitely called-methods on the + * iterated element on every path. * * @param cfg the cfg of the enclosing method * @param disposalLoop the loop to analyze @@ -3504,8 +3499,8 @@ public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoop dispos // Add an obligation for the element of the collection iterated over Obligation collectionElementObligation = Obligation.fromTree(collectionElement); - if (collectionElement instanceof VariableTree) { - VariableElement varElt = TreeUtils.elementFromDeclaration((VariableTree) collectionElement); + if (collectionElement instanceof VariableTree variableTree) { + VariableElement varElt = TreeUtils.elementFromDeclaration(variableTree); boolean hasMustCallAlias = cmAtf.hasMustCallAlias(varElt); collectionElementObligation = new Obligation( @@ -3538,8 +3533,8 @@ public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoop dispos getSuccessorsExceptIgnoredExceptions(currentBlock)) { Set obligations = new LinkedHashSet<>(current.obligations); for (Node node : currentBlock.getNodes()) { - if (node instanceof AssignmentNode) { - updateObligationsForAssignment(obligations, cfg, (AssignmentNode) node); + if (node instanceof AssignmentNode assignmentNode) { + updateObligationsForAssignment(obligations, cfg, assignmentNode); } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 7fa3d2e8d599..a7f36f05f087 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -115,354 +115,6 @@ public class RLCCalledMethodsAnnotatedTypeFactory extends CalledMethodsAnnotated */ private final BiMap tempVarToTree = HashBiMap.create(); - // /** - // * Per-method loop state for collection-obligation loops that have been matched syntactically - // by - // * the MustCall visitor. - // */ - // private static final class MethodCollectionLoopState { - // - // /** Creates per-method loop state for syntactically matched collection-obligation loops. */ - // private MethodCollectionLoopState() {} - // - // /** Enhanced-for-loops that have been matched syntactically but still need CFG resolution. - // */ - // final Set potentiallyFulfillingEnhancedForLoops = new - // LinkedHashSet<>(); - // - // /** - // * Collection-obligation loops that have been matched syntactically but still need CFG-local - // * resolution before the consistency analyzer can verify them. - // */ - // final Set potentiallyFulfillingCollectionLoops = - // new LinkedHashSet<>(); - // - // /** - // * Potentially fulfilling collection loops that have all CFG information required by the - // * consistency analyzer. - // */ - // final Set - // resolvedPotentiallyFulfillingCollectionLoops = new LinkedHashSet<>(); - // - // /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ - // private @Nullable WhileLoopResolutionCache whileLoopCache; - // - // /** - // * Returns CFG facts for resolving potentially fulfilling while loops in this method, - // creating - // * them lazily if needed. - // * - // * @param cfg the enclosing method CFG - // * @return the CFG facts for resolving potentially fulfilling while loops in this method - // */ - // private WhileLoopResolutionCache getOrCreateWhileLoopCache(ControlFlowGraph cfg) { - // if (whileLoopCache == null) { - // whileLoopCache = new WhileLoopResolutionCache(cfg); - // } - // return whileLoopCache; - // } - // } - // - // /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ - // private static final class WhileLoopResolutionCache { - // - // /** A back edge in the CFG. */ - // private static final class BlockEdge { - // /** Source block of the back edge. */ - // final Block sourceBlock; - // - // /** Target block of the back edge. */ - // final Block targetBlock; - // - // /** - // * Creates a CFG back edge description. - // * - // * @param sourceBlock source block of the back edge - // * @param targetBlock target block of the back edge - // */ - // BlockEdge(Block sourceBlock, Block targetBlock) { - // this.sourceBlock = sourceBlock; - // this.targetBlock = targetBlock; - // } - // } - // - // /** Reachable CFG blocks in the current method. */ - // private final Set reachableBlocks; - // - // /** Back edges among {@link #reachableBlocks}. */ - // private final List backEdges; - // - // /** Natural loops for back edges, computed lazily. */ - // private final IdentityHashMap> naturalLoopsByBackEdge = - // new IdentityHashMap<>(); - // - // /** - // * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. - // * - // * @param cfg the enclosing method CFG - // */ - // private WhileLoopResolutionCache(ControlFlowGraph cfg) { - // Block entryBlock = cfg.getEntryBlock(); - // this.reachableBlocks = reachableFrom(entryBlock); - // Map> dominators = computeDominators(entryBlock, reachableBlocks); - // this.backEdges = findBackEdges(reachableBlocks, dominators); - // } - // - // /** - // * Returns the back edges among the reachable blocks in the current CFG. - // * - // * @return the CFG back edges - // */ - // private List getBackEdges() { - // return backEdges; - // } - // - // /** - // * Returns the natural loop induced by the given back edge, computing it lazily if needed. - // * - // * @param backEdge the back edge - // * @return the natural loop induced by the given back edge - // */ - // private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { - // return naturalLoopsByBackEdge.computeIfAbsent( - // backEdge, - // ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); - // } - // - // /** - // * Returns blocks reachable from {@code entryBlock}. - // * - // * @param entryBlock the CFG entry block - // * @return the reachable blocks - // */ - // private static Set reachableFrom(Block entryBlock) { - // Set seen = new HashSet<>(); - // ArrayDeque queue = new ArrayDeque<>(); - // queue.add(entryBlock); - // seen.add(entryBlock); - // - // while (!queue.isEmpty()) { - // Block block = queue.remove(); - // for (Block successor : block.getSuccessors()) { - // if (successor != null && seen.add(successor)) { - // queue.add(successor); - // } - // } - // } - // return seen; - // } - - // /** - // * Computes dominators for the reachable blocks in the current CFG. - // * - // * @param entryBlock the CFG entry block - // * @param reachableBlocks reachable blocks in the CFG - // * @return dominators for each reachable block - // */ - // private static Map> computeDominators( - // Block entryBlock, Set reachableBlocks) { - // Map> dominators = new HashMap<>(); - // - // for (Block block : reachableBlocks) { - // if (block.equals(entryBlock)) { - // dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); - // } else { - // dominators.put(block, new HashSet<>(reachableBlocks)); // TOP - // } - // } - // - // boolean changed; - // do { - // changed = false; - // for (Block block : reachableBlocks) { - // if (block.equals(entryBlock)) { - // continue; - // } - // - // Set newDominators = null; - // for (Block predecessor : block.getPredecessors()) { - // if (predecessor == null || !reachableBlocks.contains(predecessor)) { - // continue; - // } - // Set predecessorDominators = dominators.get(predecessor); - // if (predecessorDominators == null) { - // continue; - // } - // if (newDominators == null) { - // newDominators = new HashSet<>(predecessorDominators); - // } else { - // newDominators.retainAll(predecessorDominators); - // } - // } - // - // if (newDominators == null) { - // newDominators = new HashSet<>(); - // } - // newDominators.add(block); - // - // if (!newDominators.equals(dominators.get(block))) { - // dominators.put(block, newDominators); - // changed = true; - // } - // } - // } while (changed); - // - // return dominators; - // } - - // /** - // * Returns the back edges among the reachable blocks in the current CFG. - // * - // * @param reachableBlocks reachable blocks in the CFG - // * @param dominators dominators for each reachable block - // * @return the CFG back edges - // */ - // private static List findBackEdges( - // Set reachableBlocks, Map> dominators) { - // List backEdges = new ArrayList<>(); - // for (Block sourceBlock : reachableBlocks) { - // for (Block targetBlock : sourceBlock.getSuccessors()) { - // if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { - // continue; - // } - // Set sourceDominators = dominators.get(sourceBlock); - // if (sourceDominators != null && sourceDominators.contains(targetBlock)) { - // // targetBlock dominates sourceBlock, so sourceBlock -> targetBlock is a back edge. - // backEdges.add(new BlockEdge(sourceBlock, targetBlock)); - // } - // } - // } - // return backEdges; - // } - - // /** - // * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. - // * - // * @param sourceBlock the source of the back edge - // * @param targetBlock the target of the back edge - // * @param reachableBlocks reachable blocks in the CFG - // * @return the natural loop induced by the back edge - // */ - // private static Set naturalLoop( - // Block sourceBlock, Block targetBlock, Set reachableBlocks) { - // Set loopBlocks = new HashSet<>(); - // ArrayDeque stack = new ArrayDeque<>(); - // - // loopBlocks.add(targetBlock); - // if (loopBlocks.add(sourceBlock)) { - // stack.push(sourceBlock); - // } - // - // while (!stack.isEmpty()) { - // Block block = stack.pop(); - // for (Block predecessor : block.getPredecessors()) { - // if (predecessor == null || !reachableBlocks.contains(predecessor)) { - // continue; - // } - // if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { - // stack.push(predecessor); - // } - // } - // } - // return loopBlocks; - // } - // } - - // /** Per-method collection-loop state accumulated during MustCall visitor matching. */ - // private final IdentityHashMap - // collectionLoopStateByEnclosingMethod = new IdentityHashMap<>(); - - // /** - // * Returns the loop state for the given method, creating it if needed. - // * - // * @param enclosingMethodTree the enclosing method - // * @return the loop state for the given method - // */ - // private MethodCollectionLoopState getOrCreateMethodCollectionLoopState( - // MethodTree enclosingMethodTree) { - // return collectionLoopStateByEnclosingMethod.computeIfAbsent( - // enclosingMethodTree, ignored -> new MethodCollectionLoopState()); - // } - - // /** - // * Returns the loop state for the given underlying AST, or {@code null} if there is none. - // * - // * @param underlyingAST the current underlying AST - // * @return the loop state for the given underlying AST, or {@code null} - // */ - // private @Nullable MethodCollectionLoopState getMethodCollectionLoopState( - // UnderlyingAST underlyingAST) { - // MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); - // if (enclosingMethodTree == null) { - // return null; - // } - // return collectionLoopStateByEnclosingMethod.get(enclosingMethodTree); - // } - - // /** - // * Returns the potentially fulfilling collection loops for the method represented by the given - // * underlying AST. - // * - // * @param underlyingAST the current underlying AST - // * @return the potentially fulfilling collection loops for the method represented by the given - // * underlying AST - // */ - // Set getPotentiallyFulfillingCollectionLoops( - // UnderlyingAST underlyingAST) { - // MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); - // if (loopState == null) { - // return Collections.emptySet(); - // } - // return loopState.potentiallyFulfillingCollectionLoops; - // } - - // /** - // * Returns the resolved potentially fulfilling collection loops for the method represented by - // the - // * given underlying AST. - // * - // * @param underlyingAST the current underlying AST - // * @return the resolved potentially fulfilling collection loops for the method represented by - // the - // * given underlying AST - // */ - // Set - // getResolvedPotentiallyFulfillingCollectionLoops( - // UnderlyingAST underlyingAST) { - // MethodCollectionLoopState loopState = getMethodCollectionLoopState(underlyingAST); - // if (loopState == null) { - // return Collections.emptySet(); - // } - // return loopState.resolvedPotentiallyFulfillingCollectionLoops; - // } - - // /** - // * Removes the loop state associated with the given underlying AST. - // * - // * @param underlyingAST the current underlying AST - // */ - // private void removeMethodCollectionLoopState(UnderlyingAST underlyingAST) { - // MethodTree enclosingMethodTree = getEnclosingMethodTree(underlyingAST); - // if (enclosingMethodTree != null) { - // collectionLoopStateByEnclosingMethod.remove(enclosingMethodTree); - // } - // } - - // /** - // * Returns the enclosing method for the given underlying AST, or {@code null} if the - // underlying - // * AST is not a method. - // * - // * @param underlyingAST the current underlying AST - // * @return the enclosing method for the given underlying AST, or {@code null} - // */ - // private @Nullable MethodTree getEnclosingMethodTree(UnderlyingAST underlyingAST) { - // if (underlyingAST.getKind() != UnderlyingAST.Kind.METHOD) { - // return null; - // } - // return ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); - // } - /** * Creates a new RLCCalledMethodsAnnotatedTypeFactory. * @@ -508,20 +160,12 @@ protected ControlFlowGraph analyze( @Nullable AccumulationStore capturedStore) { // This is a workaround for a bug that I tried and failed to fix. // See checker/tests/resourceleak/RLLambda.java. - // This code really belongs in postAnalyze, but this code only works correctly when called - // after a method is analyzed the first time and before any containing lambdas are analyzed. + // This code really belongs in postAnalyze, but this code only works correctly when called after + // a method is analyzed for the first time and before any containing lambdas are analyzed. // This workaround means there could be false positives when the type of a method invocation // depends on dataflow in a lambda. - if (cfg != null && ast.getKind() == UnderlyingAST.Kind.METHOD) { - // The cfg is not null, so the analysis has been run before. Keep that first result, but - // re-enqueue the nested classes and lambdas from the existing CFG so fixpoint still runs. - for (ClassTree cls : cfg.getDeclaredClasses()) { - classQueue.add(IPair.of(cls, getStoreBefore(cls))); - } - for (LambdaExpressionTree lambda : cfg.getDeclaredLambdas()) { - lambdaQueue.add(IPair.of(lambda, getStoreBefore(lambda))); - } + // The cfg is not null, so the analysis has been run before. Don't rerun it. return cfg; } return super.analyze( @@ -538,90 +182,15 @@ protected ControlFlowGraph analyze( @Override protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { + // Run the disposal loop discovery before called method analysis runs, as the called-method + // store needs to be initialized for the iterated loop elements temp-vars e.g., col.pop(), + // col.get(i) in order for the called-method analysis to properly track must-call obligations + // on them. if (ast.getKind() == UnderlyingAST.Kind.METHOD) { - ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this).prepareDisposalLoops(cfg); + ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this).discoverDisposalLoops(cfg); } } - // /** - // * Records a while-like collection loop that matched syntactically and still needs CFG - // resolution. - // * - // * @param enclosingMethodTree enclosing method that contains the loop - // * @param collectionTree collection expression whose elements may be discharged - // * @param collectionElementTree tree for the element extracted from the collection - // * @param conditionTree loop condition tree - // * @param loopBodyEntryBlock first block of the loop body - // * @param loopConditionalBlock conditional block that controls the loop - // * @param collectionElementNode CFG node for the extracted collection element - // */ - // public void recordPotentiallyFulfillingCollectionLoop( - // MethodTree enclosingMethodTree, - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree conditionTree, - // Block loopBodyEntryBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionElementNode) { - // getOrCreateMethodCollectionLoopState(enclosingMethodTree) - // .potentiallyFulfillingCollectionLoops - // .add( - // new PotentiallyFulfillingCollectionLoop( - // collectionTree, - // collectionElementTree, - // conditionTree, - // loopBodyEntryBlock, - // loopConditionalBlock, - // collectionElementNode)); - // } - - // /** - // * Records an enhanced-for loop that matched syntactically and still needs CFG resolution. - // * - // * @param enclosingMethodTree enclosing method that contains the loop - // * @param enhancedForLoopTree matched enhanced-for loop - // */ - // public void recordPotentiallyFulfillingEnhancedForLoop( - // MethodTree enclosingMethodTree, EnhancedForLoopTree enhancedForLoopTree) { - // getOrCreateMethodCollectionLoopState(enclosingMethodTree) - // .potentiallyFulfillingEnhancedForLoops - // .add(enhancedForLoopTree); - // } - - // /** - // * Records a collection loop whose CFG facts are fully resolved for the consistency analyzer. - // * - // * @param enclosingMethodTree enclosing method that contains the loop - // * @param collectionTree collection expression whose elements may be discharged - // * @param collectionElementTree tree for the element extracted from the collection - // * @param conditionTree loop condition tree - // * @param loopBodyEntryBlock first block of the loop body - // * @param loopUpdateBlock block that updates the loop state before the next iteration - // * @param loopConditionalBlock conditional block that controls the loop - // * @param collectionElementNode CFG node for the extracted collection element - // */ - // public void recordResolvedPotentiallyFulfillingCollectionLoop( - // MethodTree enclosingMethodTree, - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree conditionTree, - // Block loopBodyEntryBlock, - // Block loopUpdateBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionElementNode) { - // getOrCreateMethodCollectionLoopState(enclosingMethodTree) - // .resolvedPotentiallyFulfillingCollectionLoops - // .add( - // new ResolvedPotentiallyFulfillingCollectionLoop( - // collectionTree, - // collectionElementTree, - // conditionTree, - // loopBodyEntryBlock, - // loopUpdateBlock, - // loopConditionalBlock, - // collectionElementNode)); - // } - @Override protected RLCCalledMethodsAnalysis createFlowAnalysis() { return new RLCCalledMethodsAnalysis((RLCCalledMethodsChecker) checker, this); @@ -1058,485 +627,4 @@ public TransferInput getInput(Block block) return analysis.getInput(block); } } - // - // /** Wrapper for a loop that potentially calls methods on all elements of a collection/array. - // */ - // public static class PotentiallyFulfillingCollectionLoop { - // - // /** AST {@code Tree} for collection iterated over. */ - // public final ExpressionTree collectionTree; - // - // /** AST {@code Tree} for collection element iterated over. */ - // public final Tree collectionElementTree; - // - // /** AST {@code Tree} for loop condition. */ - // public final Tree condition; - // - // /** cfg {@code Block} containing the loop body entry. */ - // public final Block loopBodyEntryBlock; - // - // /** cfg conditional {@link Block} following loop condition. */ - // public final ConditionalBlock loopConditionalBlock; - // - // /** cfg {@code Node} for the collection element iterated over. */ - // public final Node collectionElementNode; - // - // /** - // * Constructs a new {@code PotentiallyFulfillingCollectionLoop}. - // * - // * @param collectionTree AST {@link Tree} for collection iterated over - // * @param collectionElementTree AST {@link Tree} for collection element iterated over - // * @param condition AST {@link Tree} for loop condition - // * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry - // * @param loopConditionalBlock cfg conditional {@link Block} following loop condition - // * @param collectionEltNode cfg {@link Node} for the collection element iterated over - // */ - // public PotentiallyFulfillingCollectionLoop( - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree condition, - // Block loopBodyEntryBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionEltNode) { - // this.collectionTree = collectionTree; - // this.collectionElementTree = collectionElementTree; - // this.condition = condition; - // this.loopBodyEntryBlock = loopBodyEntryBlock; - // this.loopConditionalBlock = loopConditionalBlock; - // this.collectionElementNode = collectionEltNode; - // } - // } - // - // /** - // * A potentially fulfilling collection loop whose CFG-local information is complete enough for - // * consistency analysis. - // */ - // public static class ResolvedPotentiallyFulfillingCollectionLoop - // extends PotentiallyFulfillingCollectionLoop { - // - // /** - // * The methods that the loop definitely calls on all elements of the collection it iterates - // * over. - // */ - // protected final Set calledMethods; - // - // /** cfg {@code Block} containing the loop update. */ - // public final Block loopUpdateBlock; - // - // /** - // * Constructs a new {@code ResolvedPotentiallyFulfillingCollectionLoop}. - // * - // * @param collectionTree AST {@link Tree} for collection iterated over - // * @param collectionElementTree AST {@link Tree} for collection element iterated over - // * @param condition AST {@link Tree} for loop condition - // * @param loopBodyEntryBlock cfg {@link Block} for the loop body entry - // * @param loopUpdateBlock cfg {@link Block} for the loop update - // * @param loopConditionalBlock cfg conditional {@link Block} following loop condition - // * @param collectionEltNode cfg {@link Node} for the collection element iterated over - // */ - // public ResolvedPotentiallyFulfillingCollectionLoop( - // ExpressionTree collectionTree, - // Tree collectionElementTree, - // Tree condition, - // Block loopBodyEntryBlock, - // Block loopUpdateBlock, - // ConditionalBlock loopConditionalBlock, - // Node collectionEltNode) { - // super( - // collectionTree, - // collectionElementTree, - // condition, - // loopBodyEntryBlock, - // loopConditionalBlock, - // collectionEltNode); - // this.calledMethods = new HashSet<>(); - // this.loopUpdateBlock = loopUpdateBlock; - // } - - // /** - // * Add methods that are guaranteed to be invoked on every element of the collection the loop - // * iterates over. - // * - // * @param methods the set of methods to add - // */ - // public void addCalledMethods(Set methods) { - // calledMethods.addAll(methods); - // } - - // /** - // * Returns methods that are guaranteed to be invoked on every element of the collection the - // loop - // * iterates over. - // * - // * @return the set of methods the loop calls on all elements of the iterated collection - // */ - // public Set getCalledMethods() { - // return calledMethods; - // } - // } - - // @Override - // public void postAnalyze(ControlFlowGraph cfg) { - // super.postAnalyze(cfg); - // removeMethodCollectionLoopState(cfg.getUnderlyingAST()); - // } - - // /** - // * Returns blocks reachable from {@code entryBlock}. - // * - // *

This remains a utility on the outer type because the resource leak consistency analyzer - // also - // * uses it. - // * - // * @param entryBlock the CFG entry block - // * @return the reachable blocks - // */ - // public static Set reachableFrom(Block entryBlock) { - // return WhileLoopResolutionCache.reachableFrom(entryBlock); - // } - - // /** - // * Analyzes CFG-resolved potentially fulfilling collection loops for the current method and - // * removes the ones that were analyzed. - // * - // * @param cfg the CFG of the current method - // * @param loopState per-method collection-loop state - // * @param mustCallConsistencyAnalyzer the consistency analyzer - // */ - // @SuppressWarnings("UnusedMethod") - // private void analyzeResolvedPotentiallyFulfillingCollectionLoops( - // ControlFlowGraph cfg, - // MethodCollectionLoopState loopState, - // MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer) { - // if (loopState.resolvedPotentiallyFulfillingCollectionLoops.isEmpty()) { - // return; - // } - // - // Iterator resolvedLoopIterator = - // loopState.resolvedPotentiallyFulfillingCollectionLoops.iterator(); - // while (resolvedLoopIterator.hasNext()) { - // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = resolvedLoopIterator.next(); - // Tree collectionElementTree = resolvedLoop.collectionElementTree; - // boolean loopContainedInThisMethod = - // cfg.getNodesCorrespondingToTree(collectionElementTree) != null; - // if (loopContainedInThisMethod) { - // DisposalLoop disposalLoop = - // new DisposalLoop( - // resolvedLoop.collectionTree, - // resolvedLoop.collectionElementTree, - // resolvedLoop.collectionElementNode, - // resolvedLoop.condition, - // resolvedLoop.loopConditionalBlock, - // resolvedLoop.loopBodyEntryBlock, - // resolvedLoop.loopUpdateBlock); - // Set disposalLoopCalledMethods = - // mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoop); - // if (disposalLoopCalledMethods != null) { - // ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this) - // .registerCalledMethodsForDisposalLoop(resolvedLoop, disposalLoopCalledMethods); - // } - // resolvedLoopIterator.remove(); - // } - // } - // } - // - // /** Resolves enhanced-for-loop candidates into CFG-resolved loops for consistency analysis. */ - // @SuppressWarnings({"UnusedNestedClass", "UnusedMethod"}) - // private final class EnhancedForLoopResolver { - // /** The CFG of the current method. */ - // private final ControlFlowGraph cfg; - // - // /** Per-method collection-loop state. */ - // private final MethodCollectionLoopState loopState; - // - // /** Blocks that have already been visited while traversing the CFG. */ - // private final Set visitedBlocks = new HashSet<>(); - // - // /** Worklist for CFG traversal. */ - // private final Deque worklist = new ArrayDeque<>(); - // - // /** - // * Creates a resolver for enhanced-for-loops in the given CFG. - // * - // * @param cfg the CFG of the current method - // * @param loopState per-method collection-loop state - // */ - // private EnhancedForLoopResolver(ControlFlowGraph cfg, MethodCollectionLoopState loopState) { - // this.cfg = cfg; - // this.loopState = loopState; - // } - // - // /** Traverses the CFG and records resolved enhanced-for-loops for the pending candidates. */ - // private void resolveEnhancedForLoops() { - // Block entryBlock = cfg.getEntryBlock(); - // worklist.add(entryBlock); - // visitedBlocks.add(entryBlock); - // - // while (!worklist.isEmpty() && !loopState.potentiallyFulfillingEnhancedForLoops.isEmpty()) - // { - // Block currentBlock = worklist.removeFirst(); - // - // for (Node node : currentBlock.getNodes()) { - // if (node instanceof MethodInvocationNode) { - // resolveEnhancedForLoop((MethodInvocationNode) node); - // } - // } - // - // for (IPair successorAndExceptionType : - // getSuccessorsExceptIgnoredExceptions(currentBlock)) { - // Block successorBlock = successorAndExceptionType.first; - // if (successorBlock != null && visitedBlocks.add(successorBlock)) { - // worklist.addLast(successorBlock); - // } - // } - // } - // } - // - // /** - // * Returns all successor blocks for some block, except for those corresponding to ignored - // * exception types. See {@link RLCCalledMethodsAnalysis#isIgnoredExceptionType(TypeMirror)}. - // * - // * @param block input block - // * @return set of pairs (b, t), where b is a successor block, and t is the type of exception - // for - // * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor - // */ - // private Set> getSuccessorsExceptIgnoredExceptions( - // Block block) { - // if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { - // ExceptionBlock exceptionBlock = (ExceptionBlock) block; - // Set> result = new LinkedHashSet<>(); - // Block regularSuccessor = exceptionBlock.getSuccessor(); - // if (regularSuccessor != null) { - // result.add(IPair.of(regularSuccessor, null)); - // } - // Map> exceptionalSuccessors = - // exceptionBlock.getExceptionalSuccessors(); - // for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { - // TypeMirror exceptionType = entry.getKey(); - // if (!isIgnoredExceptionType(exceptionType)) { - // for (Block exceptionalSuccessor : entry.getValue()) { - // result.add(IPair.of(exceptionalSuccessor, exceptionType)); - // } - // } - // } - // return result; - // } else { - // Set> result = new LinkedHashSet<>(); - // for (Block successorBlock : block.getSuccessors()) { - // result.add(IPair.of(successorBlock, null)); - // } - // return result; - // } - // } - - // /** - // * Records a resolved collection loop if the given node is desugared from an - // enhanced-for-loop - // * over a collection. - // * - // * @param methodInvocationNode the node to check - // */ - // private void resolveEnhancedForLoop(MethodInvocationNode methodInvocationNode) { - // if (methodInvocationNode.getIterableExpression() == null) { - // return; - // } - // - // EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); - // if (loop == null) { - // throw new BugInCF( - // "MethodInvocationNode.iterableExpression should be non-null iff" - // + " MethodInvocationNode.enhancedForLoop is non-null"); - // } - // if (!loopState.potentiallyFulfillingEnhancedForLoops.contains(loop)) { - // return; - // } - // - // VariableTree loopVariable = loop.getVariable(); - // - // // Find the first block of the loop body by traversing the desugared iterator.next() path - // // until the assignment of the loop variable is found. - // SingleSuccessorBlock singleSuccessorBlock = - // (SingleSuccessorBlock) methodInvocationNode.getBlock(); - // Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); - // Node loopVariableNode = null; - // Node node; - // boolean isAssignmentOfLoopVariable; - // do { - // while (!nodeIterator.hasNext()) { - // singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); - // nodeIterator = singleSuccessorBlock.getNodes().iterator(); - // } - // node = nodeIterator.next(); - // isAssignmentOfLoopVariable = false; - // if ((node instanceof AssignmentNode) && (node.getTree() instanceof VariableTree)) { - // loopVariableNode = ((AssignmentNode) node).getTarget(); - // VariableTree iteratorVariableDeclaration = (VariableTree) node.getTree(); - // isAssignmentOfLoopVariable = - // iteratorVariableDeclaration.getName() == loopVariable.getName(); - // } - // } while (!isAssignmentOfLoopVariable); - // Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); - // - // // Find the desugared loop condition by traversing the CFG backwards until - // iterator.hasNext() - // // is found. - // Block loopUpdateBlock = methodInvocationNode.getBlock(); - // nodeIterator = loopUpdateBlock.getNodes().iterator(); - // boolean isLoopCondition; - // do { - // while (!nodeIterator.hasNext()) { - // Set predecessorBlocks = loopUpdateBlock.getPredecessors(); - // if (predecessorBlocks.size() == 1) { - // loopUpdateBlock = predecessorBlocks.iterator().next(); - // nodeIterator = loopUpdateBlock.getNodes().iterator(); - // } else { - // // There is no trivial resolution here. Best we can do is skip this loop. - // return; - // } - // } - // node = nodeIterator.next(); - // isLoopCondition = false; - // if (node instanceof MethodInvocationNode) { - // MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); - // isLoopCondition = TreeUtils.isHasNextCall(methodInvocationTree); - // } - // } while (!isLoopCondition); - // - // Block blockContainingLoopCondition = node.getBlock(); - // if (blockContainingLoopCondition.getSuccessors().size() != 1) { - // throw new BugInCF( - // "loop condition has: " - // + blockContainingLoopCondition.getSuccessors().size() - // + " successors instead of 1."); - // } - // Block conditionalBlock = blockContainingLoopCondition.getSuccessors().iterator().next(); - // if (!(conditionalBlock instanceof ConditionalBlock)) { - // throw new BugInCF( - // "loop condition successor is not ConditionalBlock, but: " - // + conditionalBlock.getClass()); - // } - // - // loopState.resolvedPotentiallyFulfillingCollectionLoops.add( - // new ResolvedPotentiallyFulfillingCollectionLoop( - // loop.getExpression(), - // loopVariableNode.getTree(), - // node.getTree(), - // loopBodyEntryBlock, - // loopUpdateBlock, - // (ConditionalBlock) conditionalBlock, - // loopVariableNode)); - // loopState.potentiallyFulfillingEnhancedForLoops.remove(loop); - // } - // } - // - // /** Resolves while-loop candidates into CFG-resolved loops for consistency analysis. */ - // @SuppressWarnings({"UnusedNestedClass", "UnusedMethod"}) - // private static final class WhileLoopResolver { - // /** The CFG of the current method. */ - // private final ControlFlowGraph cfg; - // - // /** - // * Creates a resolver for potentially fulfilling while loops in the given CFG. - // * - // * @param cfg the enclosing method CFG - // */ - // private WhileLoopResolver(ControlFlowGraph cfg) { - // this.cfg = cfg; - // } - // - // /** - // * Resolves all potentially fulfilling while loops in the given method state that can be - // tied to - // * a loop update block in the current CFG. - // * - // * @param loopState per-method collection-loop state - // */ - // private void resolveWhileLoops(MethodCollectionLoopState loopState) { - // if (loopState.potentiallyFulfillingCollectionLoops.isEmpty()) { - // return; - // } - // - // WhileLoopResolutionCache whileLoopCache = loopState.getOrCreateWhileLoopCache(cfg); - // - // Iterator potentialLoopIterator = - // loopState.potentiallyFulfillingCollectionLoops.iterator(); - // while (potentialLoopIterator.hasNext()) { - // PotentiallyFulfillingCollectionLoop potentialLoop = potentialLoopIterator.next(); - // ResolvedPotentiallyFulfillingCollectionLoop resolvedLoop = - // resolveWhileLoop(potentialLoop, whileLoopCache); - // if (resolvedLoop != null) { - // loopState.resolvedPotentiallyFulfillingCollectionLoops.add(resolvedLoop); - // potentialLoopIterator.remove(); - // } - // } - // } - // - // /** - // * Resolves one potentially fulfilling while loop if a suitable loop update block can be - // found. - // * - // * @param potentialLoop a potentially fulfilling while loop - // * @param whileLoopCache cached CFG facts for while-loop resolution - // * @return the CFG-resolved loop, or {@code null} if no loop update block is found - // */ - // private @Nullable ResolvedPotentiallyFulfillingCollectionLoop resolveWhileLoop( - // PotentiallyFulfillingCollectionLoop potentialLoop, - // WhileLoopResolutionCache whileLoopCache) { - // Block loopUpdateBlock = - // chooseLoopUpdateBlockForPotentiallyFulfillingLoop(potentialLoop, whileLoopCache); - // if (loopUpdateBlock == null) { - // return null; - // } - // return new ResolvedPotentiallyFulfillingCollectionLoop( - // potentialLoop.collectionTree, - // potentialLoop.collectionElementTree, - // potentialLoop.condition, - // potentialLoop.loopBodyEntryBlock, - // loopUpdateBlock, - // potentialLoop.loopConditionalBlock, - // potentialLoop.collectionElementNode); - // } - // - // /** - // * Chooses the best loop update block for a potentially fulfilling while loop by matching it - // to - // * the tightest natural loop that contains both the body entry and the loop condition. - // * - // * @param potentialLoop a potentially fulfilling while loop - // * @param whileLoopCache cached CFG facts for while-loop resolution - // * @return the chosen loop update block, or {@code null} if none is found - // */ - // private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( - // PotentiallyFulfillingCollectionLoop potentialLoop, - // WhileLoopResolutionCache whileLoopCache) { - // - // Block bodyEntryBlock = potentialLoop.loopBodyEntryBlock; - // Block conditionalBlock = potentialLoop.loopConditionalBlock; - // - // Block bestLoopUpdateBlock = null; - // int bestLoopSize = Integer.MAX_VALUE; - // - // for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { - // // backEdge.targetBlock is the candidate block that the loop body flows back to. - // Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); - // - // // Must contain this while-loop's body entry and conditional block. - // if (!naturalLoop.contains(bodyEntryBlock)) { - // continue; - // } - // if (!naturalLoop.contains(conditionalBlock)) { - // continue; - // } - // - // // Prefer the tightest loop. This helps nested-loop disambiguation. - // if (naturalLoop.size() < bestLoopSize) { - // bestLoopSize = naturalLoop.size(); - // bestLoopUpdateBlock = backEdge.targetBlock; - // } - // } - // - // return bestLoopUpdateBlock; - // } - // } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index f9c00abf741d..0917f59f7842 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -62,8 +62,8 @@ public void accumulate( } /** - * Add the collection elements iterated over in potentially fulfilling collection loops to the - * store. + * Add the collection elements iterated over in {@link DisposalLoop}'s to the store so that + * temp-vars. e.g., col.get(i), col.pop() are tracked. */ @Override public AccumulationStore initialStore( @@ -71,7 +71,7 @@ public AccumulationStore initialStore( AccumulationStore store = super.initialStore(underlyingAST, parameters); for (DisposalLoop disposalLoop : ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(rlTypeFactory) - .getPreparedDisposalLoops(underlyingAST)) { + .getDisposalLoops(underlyingAST)) { IteratedCollectionElement collectionElementJE = new IteratedCollectionElement( disposalLoop.iteratedElementNode, disposalLoop.iteratedElementTree); diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollectionsTest.java similarity index 81% rename from checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollectionsTest.java index 6642b6eb3d8d..1746576c4d41 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollectionsTest.java @@ -7,8 +7,8 @@ import org.junit.runners.Parameterized.Parameters; /** Tests for the Resource Leak Checker for Collections. */ -public class ResourceLeakCollections extends CheckerFrameworkPerDirectoryTest { - public ResourceLeakCollections(List testFiles) { +public class ResourceLeakCollectionsTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakCollectionsTest(List testFiles) { super( testFiles, ResourceLeakChecker.class, diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java index 7e88b6139cfb..8836cdbeea92 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java @@ -34,10 +34,9 @@ public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } - if (!(obj instanceof IteratedCollectionElement)) { + if (!(obj instanceof IteratedCollectionElement other)) { return false; } - IteratedCollectionElement other = (IteratedCollectionElement) obj; return this.tree.equals(other.tree) && this.node.equals(other.node); } @@ -59,10 +58,9 @@ public boolean isDeterministic(AnnotationProvider provider) { @Override public boolean syntacticEquals(JavaExpression je) { - if (!(je instanceof IteratedCollectionElement)) { + if (!(je instanceof IteratedCollectionElement other)) { return false; } - IteratedCollectionElement other = (IteratedCollectionElement) je; return this.equals(other); } diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index 52a551ab9097..789c47a281ed 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -664,8 +664,7 @@ protected void computeNewValueAndInsert( if (newValue != null) { localVariableValues.put(localVar, newValue); } - } else if (expr instanceof IteratedCollectionElement) { - IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + } else if (expr instanceof IteratedCollectionElement collectionElt) { V oldValue = iteratedCollectionElements.get(collectionElt); V newValue = merger.apply(oldValue, value); if (newValue != null) { @@ -814,8 +813,7 @@ public void clearValue(JavaExpression expr) { } if (expr instanceof LocalVariable localVar) { localVariableValues.remove(localVar); - } else if (expr instanceof IteratedCollectionElement) { - IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + } else if (expr instanceof IteratedCollectionElement collectionElt) { iteratedCollectionElements.remove(collectionElt); } else if (expr instanceof FieldAccess fieldAcc) { fieldValues.remove(fieldAcc); @@ -859,8 +857,7 @@ public void clearValue(JavaExpression expr) { public @Nullable V getValue(JavaExpression expr) { if (expr instanceof LocalVariable localVar) { return localVariableValues.get(localVar); - } else if (expr instanceof IteratedCollectionElement) { - IteratedCollectionElement collectionElt = (IteratedCollectionElement) expr; + } else if (expr instanceof IteratedCollectionElement collectionElt) { return iteratedCollectionElements.get(collectionElt); } else if (expr instanceof ThisReference || expr instanceof SuperReference) { return thisValue; diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 4a6239e185f4..0029565278dc 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -1694,9 +1694,8 @@ && getFieldName(tree).equals("length")) { * @return ExpressionTree of {@code idx} if tree is {@code Collection.get(idx)} and null else */ public static @Nullable ExpressionTree getIdxForGetCall(Tree tree) { - if ((tree instanceof MethodInvocationTree) - && isNamedMethodCall("get", (MethodInvocationTree) tree)) { - return ((MethodInvocationTree) tree).getArguments().get(0); + if ((tree instanceof MethodInvocationTree mit) && isNamedMethodCall("get", mit)) { + return mit.getArguments().get(0); } return null; } @@ -1708,9 +1707,9 @@ && isNamedMethodCall("get", (MethodInvocationTree) tree)) { * @return true if the given tree is of the form object.size() */ public static boolean isSizeAccess(Tree tree) { - return (tree instanceof MethodInvocationTree) - && isNamedMethodCall("size", (MethodInvocationTree) tree) - && ((MethodInvocationTree) tree).getArguments().isEmpty(); + return (tree instanceof MethodInvocationTree mit) + && isNamedMethodCall("size", mit) + && mit.getArguments().isEmpty(); } /** From a4c20aadd168ae866731824248a615766d73e58a Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Mon, 27 Apr 2026 21:31:55 -0700 Subject: [PATCH 365/374] CI failure fix. --- .../EnhancedForDisposalLoopResolver.java | 3 ++- .../IndexedForDisposalLoopMatcher.java | 8 ++------ .../collectionownership/WhileDisposalLoopMatcher.java | 10 +++------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java index d09f199aa35b..4323309c8170 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -10,6 +10,7 @@ import java.util.Iterator; import java.util.Set; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.block.Block; @@ -107,7 +108,7 @@ final class EnhancedForDisposalLoopResolver { * @return the resolved loop, or {@code null} if the node does not belong to {@code tree} */ private @Nullable DisposalLoop resolveEnhancedForLoop( - MethodInvocationNode methodInvocationNode, EnhancedForLoopTree tree) { + MethodInvocationNode methodInvocationNode, @FindDistinct EnhancedForLoopTree tree) { if (methodInvocationNode.getIterableExpression() == null) { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index 2508379d4946..500d3375d0a8 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -17,10 +17,8 @@ import com.sun.source.util.TreeScanner; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import javax.lang.model.element.Element; import javax.lang.model.element.Name; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; @@ -153,8 +151,7 @@ Name nameOfCollectionThatAllElementsAreCalledOn( ExpressionTree methodSelect = ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); if (methodSelect instanceof MemberSelectTree mst) { - Element elt = TreeUtils.elementFromTree(mst.getExpression()); - if (ResourceLeakUtils.isCollection(elt, coAtf)) { + if (coAtf.isResourceCollection(mst.getExpression())) { return CollectionOwnershipUtils.getNameFromExpressionTree(mst.getExpression()); } } @@ -255,8 +252,7 @@ private boolean isIthCollectionElement(Tree tree, Name index) { TreeUtils.getIdxForGetCall(tree))) { ExpressionTree methodSelect = mit.getMethodSelect(); if (methodSelect instanceof MemberSelectTree mst) { - Element receiverElt = TreeUtils.elementFromTree(mst.getExpression()); - return ResourceLeakUtils.isCollection(receiverElt, coAtf); + return coAtf.isResourceCollection(mst.getExpression()); } } return false; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index 85f73542f405..684f857237bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -28,7 +28,6 @@ import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.ResourceLeakUtils; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.block.Block; @@ -475,8 +474,7 @@ private static final class BodyExtraction { if (varName == null) { return null; } - Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, coAtf)) { + if (!coAtf.isResourceCollection(recv)) { return null; } ExpressionTree colTree = CollectionOwnershipUtils.baseExpression(recv); @@ -536,8 +534,7 @@ private static final class BodyExtraction { return null; } - Element recvElt = TreeUtils.elementFromTree(recv); - if (!ResourceLeakUtils.isCollection(recvElt, coAtf)) { + if (!coAtf.isResourceCollection(recv)) { return null; } @@ -621,8 +618,7 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { } ExpressionTree colExpr = ms.getExpression(); - Element colElt = TreeUtils.elementFromTree(colExpr); - if (!ResourceLeakUtils.isCollection(colElt, coAtf)) { + if (!coAtf.isResourceCollection(colExpr)) { return null; } From 2ab0f26e0aa484ba2e0692fa2a3f1a681efea0c9 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Tue, 28 Apr 2026 00:50:09 -0700 Subject: [PATCH 366/374] CI failure fix. --- .../OwningCollectionFieldTest.java | 8 -------- checker/tests/resourceleak-collections/Resource.java | 10 ++++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 checker/tests/resourceleak-collections/Resource.java diff --git a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java index 90fd1ce3f438..01ffe35fcc2c 100644 --- a/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java +++ b/checker/tests/resourceleak-collections/OwningCollectionFieldTest.java @@ -5,14 +5,6 @@ import org.checkerframework.checker.collectionownership.qual.*; import org.checkerframework.checker.mustcall.qual.*; -@InheritableMustCall({"flush", "close"}) -class Resource implements AutoCloseable { - @Override - public void close() {} - - void flush() {} -} - // 2. check that Aggregator has MustCall method class Aggregator implements Closeable { // 1. infer this field as @OwningCollection diff --git a/checker/tests/resourceleak-collections/Resource.java b/checker/tests/resourceleak-collections/Resource.java new file mode 100644 index 000000000000..74f750863ae5 --- /dev/null +++ b/checker/tests/resourceleak-collections/Resource.java @@ -0,0 +1,10 @@ +// A class that represents an autoclosable resource class, used by other test files. +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall({"flush", "close"}) +class Resource implements AutoCloseable { + @Override + public void close() {} + + void flush() {} +} From 4f134ebd9ba83e8acc15b0bfe0fac662c585fe0e Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 7 May 2026 01:26:47 -0700 Subject: [PATCH 367/374] resolves code review comments. --- .../classfile/ClassAnnotationSceneWriter.java | 7 +- .../util/coll/LinkedHashKeyedSet.java | 6 +- .../qual/CreatesCollectionObligation.java | 3 +- .../CollectionOwnershipAnalysis.java | 5 +- ...llectionOwnershipAnnotatedTypeFactory.java | 120 +++++++++-------- .../CollectionOwnershipTransfer.java | 23 +++- .../CollectionOwnershipUtils.java | 28 +--- .../collectionownership/DisposalLoop.java | 66 ---------- .../collectionownership/DisposalLoopInfo.java | 61 +++++++++ .../DisposalLoopScanner.java | 30 ++--- .../EnhancedForDisposalLoopResolver.java | 124 ++++++++++++------ .../IndexedForDisposalLoopMatcher.java | 20 ++- .../WhileDisposalLoopMatcher.java | 15 ++- .../MustCallConsistencyAnalyzer.java | 38 +++--- .../resourceleak/ResourceLeakUtils.java | 4 +- .../RLCCalledMethodsAnnotatedTypeFactory.java | 3 +- .../RLCCalledMethodsTransfer.java | 10 +- .../diagnostics/JavaDiagnosticReader.java | 4 +- 18 files changed, 303 insertions(+), 264 deletions(-) delete mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java create mode 100644 checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java index 5d30839ffce2..6ee8b69f7b49 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/classfile/ClassAnnotationSceneWriter.java @@ -27,7 +27,6 @@ import org.checkerframework.afu.scenelib.field.ArrayAFT; import org.checkerframework.afu.scenelib.field.ClassTokenAFT; import org.checkerframework.afu.scenelib.field.EnumAFT; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; @@ -431,8 +430,7 @@ public FieldAnnotationSceneWriter(int api, String name, FieldVisitor fv) { } @Override - @SuppressWarnings("nullness:override.return") // ASM lacks (some?) @Nullable annotations - public @Nullable AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { existingFieldAnnotations.add(descriptor); // If annotation exists in scene, and in overwrite mode, @@ -445,8 +443,7 @@ public FieldAnnotationSceneWriter(int api, String name, FieldVisitor fv) { } @Override - @SuppressWarnings("nullness:override.return") // ASM lacks (some?) @Nullable annotations - public @Nullable AnnotationVisitor visitTypeAnnotation( + public AnnotationVisitor visitTypeAnnotation( int typeRef, TypePath typePath, String descriptor, boolean visible) { // typeRef: FIELD existingFieldAnnotations.add(descriptor); diff --git a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java index d535d7410164..fe5f0c124223 100644 --- a/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java +++ b/annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java @@ -7,7 +7,6 @@ import org.checkerframework.checker.collectionownership.qual.NotOwningCollection; import org.checkerframework.checker.collectionownership.qual.PolyOwningCollection; import org.checkerframework.checker.mustcall.qual.NotOwning; -import org.checkerframework.checker.mustcall.qual.Owning; /** * A simple implementation of {@link KeyedSet} backed by an insertion-order {@link @@ -67,7 +66,8 @@ public void remove() { } @Override - public Iterator iterator(@PolyOwningCollection LinkedHashKeyedSet this) { + public @PolyOwningCollection Iterator iterator( + @PolyOwningCollection LinkedHashKeyedSet this) { return new KeyedSetIterator(); } @@ -121,7 +121,7 @@ public V add( } @Override - public boolean add(@Owning V o) { + public boolean add(V o) { return add(o, THROW_EXCEPTION, IGNORE) == null; } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java index ad68ff3246c2..c13ed0ce776f 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java @@ -17,8 +17,7 @@ * the receiver. * *

This annotation should only be used on method declarations of collections, as defined by the - * CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code java.util.Iterator} - * implementations. + * CollectionOwnershipChecker, that is, {@code java.lang.Iterable} implementations. * * @checker_framework.manual #resource-leak-checker Resource Leak Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java index d9b44db3b3c6..abd51ac40ccc 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java @@ -38,8 +38,9 @@ public CollectionOwnershipAnalysis( /** * Creates the ignored-exception policy used by collection-ownership flow. * - *

The policy follows the Resource Leak Checker configuration, but keeps exact{@link Throwable} - * exceptional edges because broad catch/fallback paths affect collection-obligation reasoning. + *

The policy follows the Resource Leak Checker configuration, but keeps exact {@link + * Throwable} exceptional edges because broad catch/fallback paths affect collection-obligation + * reasoning. * * @param checker the enclosing checker * @return the ignored-exception policy for collection-ownership flow diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index a549830bd3ba..e10a122f6189 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -87,26 +87,27 @@ public class CollectionOwnershipAnnotatedTypeFactory */ private final MustCallAnnotatedTypeFactory mcAtf; - /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoop}. */ - private final IdentityHashMap conditionToDisposalLoopMap = + /** Map from a loop-condition {@code Tree} to its corresponding {@link DisposalLoopInfo}. */ + private final IdentityHashMap conditionToDisposalLoopInfoMap = new IdentityHashMap<>(); - /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoop}. */ - private final IdentityHashMap conditionalBlockToDisposalLoopMap = + /** Map from a loop's conditional {@code Block} to its corresponding {@link DisposalLoopInfo}. */ + private final IdentityHashMap conditionalBlockToDisposalLoopInfoMap = new IdentityHashMap<>(); /** - * Map from a {@link DisposalLoop} to the set of called-methods on the iterated element in its + * Map from a {@link DisposalLoopInfo} to the set of called-methods on the iterated element in its * loop body. */ - private final IdentityHashMap> disposalLoopToCalledMethodsMap = + private final IdentityHashMap> disposalLoopInfoToCalledMethodsMap = new IdentityHashMap<>(); /** - * Map from a {@code MethodTree} to the {@link DisposalLoop}s discovered in that method's body. + * Map from a {@code MethodTree} to the {@link DisposalLoopInfo}s discovered in that method's + * body. */ - private final IdentityHashMap> preparedDisposalLoopsByMethod = - new IdentityHashMap<>(); + private final IdentityHashMap> + preparedDisposalLoopInfosByMethod = new IdentityHashMap<>(); /** The {@code @}{@link NotOwningCollection} annotation. */ public final AnnotationMirror TOP; @@ -194,36 +195,38 @@ public CollectionOwnershipAnnotatedTypeFactory(BaseTypeChecker checker) { } /** - * Returns the {@link DisposalLoop} corresponding to the loop condition {@code tree}, if one + * Returns the {@link DisposalLoopInfo} corresponding to the loop condition {@code tree}, if one * exists. * * @param tree the condition tree - * @return the {@link DisposalLoop} for condition {@code tree} if exists, otherwise {@code null}. + * @return the {@link DisposalLoopInfo} for condition {@code tree} if exists, otherwise {@code + * null}. */ - public @Nullable DisposalLoop getDisposalLoopForConditionTree(Tree tree) { - return conditionToDisposalLoopMap.get(tree); + public @Nullable DisposalLoopInfo getDisposalLoopInfoForConditionTree(Tree tree) { + return conditionToDisposalLoopInfoMap.get(tree); } /** - * Returns the {@link DisposalLoop} corresponding to the loop conditional {@code block}, if one - * exists. + * Returns the {@link DisposalLoopInfo} corresponding to the loop conditional {@code block}, if + * one exists. * * @param block the loop-condition block - * @return the {@link DisposalLoop} for conditional {@code block} if exists, otherwise {@code + * @return the {@link DisposalLoopInfo} for conditional {@code block} if exists, otherwise {@code * null}. */ - public @Nullable DisposalLoop getDisposalLoopForConditionBlock(Block block) { - return conditionalBlockToDisposalLoopMap.get(block); + public @Nullable DisposalLoopInfo getDisposalLoopInfoForConditionBlock(Block block) { + return conditionalBlockToDisposalLoopInfoMap.get(block); } /** - * Returns the called-methods on the iterated element for a {@link DisposalLoop}. + * Returns the called-methods on the iterated element for a {@link DisposalLoopInfo}. * - * @param disposalLoop the {@link DisposalLoop} - * @return the called-methods for {@link DisposalLoop}, or {@code null} if no information exits + * @param disposalLoopInfo the {@link DisposalLoopInfo} + * @return the called-methods for {@link DisposalLoopInfo}, or {@code null} if no information + * exits */ - public @Nullable Set getCalledMethods(DisposalLoop disposalLoop) { - return disposalLoopToCalledMethodsMap.get(disposalLoop); + public @Nullable Set getCalledMethods(DisposalLoopInfo disposalLoopInfo) { + return disposalLoopInfoToCalledMethodsMap.get(disposalLoopInfo); } /** @@ -239,27 +242,28 @@ public boolean isIgnoredExceptionType(TypeMirror exceptionType) { } /** - * Registers a {@link DisposalLoop} together with the called-methods on the iterated element over - * that loop's body. + * Registers a {@link DisposalLoopInfo} together with the called-methods on the iterated element + * over that loop's body. * - * @param disposalLoop the {@link DisposalLoop} - * @param calledMethods the called-methods on the iterated element over {@link DisposalLoop}'s + * @param disposalLoopInfo the {@link DisposalLoopInfo} + * @param calledMethods the called-methods on the iterated element over {@link DisposalLoopInfo}'s * body */ - private void registerCalledMethods(DisposalLoop disposalLoop, Set calledMethods) { - conditionToDisposalLoopMap.put(disposalLoop.loopConditionTree, disposalLoop); - conditionalBlockToDisposalLoopMap.put(disposalLoop.loopConditionalBlock, disposalLoop); - disposalLoopToCalledMethodsMap.put( - disposalLoop, Collections.unmodifiableSet(new LinkedHashSet<>(calledMethods))); + private void registerCalledMethods(DisposalLoopInfo disposalLoopInfo, Set calledMethods) { + conditionToDisposalLoopInfoMap.put(disposalLoopInfo.loopConditionTree(), disposalLoopInfo); + conditionalBlockToDisposalLoopInfoMap.put( + disposalLoopInfo.loopConditionalBlock(), disposalLoopInfo); + disposalLoopInfoToCalledMethodsMap.put( + disposalLoopInfo, Collections.unmodifiableSet(new LinkedHashSet<>(calledMethods))); } /** - * Scans a method CFG for {@link DisposalLoop}'s and returns the discovered loops. + * Scans a method CFG for {@link DisposalLoopInfo}'s and returns the discovered loops. * * @param cfg the CFG to scan - * @return the {@link DisposalLoop}'s discovered in {@code cfg} + * @return the {@link DisposalLoopInfo}'s discovered in {@code cfg} */ - private Set scanForDisposalLoops(ControlFlowGraph cfg) { + private Set scanForDisposalLoopInfos(ControlFlowGraph cfg) { if (cfg.getUnderlyingAST().getKind() != UnderlyingAST.Kind.METHOD) { return Collections.emptySet(); } @@ -269,56 +273,59 @@ private Set scanForDisposalLoops(ControlFlowGraph cfg) { } /** - * Discovers and stores the {@link DisposalLoop}'s for a method {@code cfg}. This discovery must - * happen before {@link + * Discovers and stores the {@link DisposalLoopInfo}'s for a method {@code cfg}. This discovery + * must happen before {@link * org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsTransfer#initialStore(UnderlyingAST, * List)} runs, so that the called-methods initial stores are populated for the loop's iterated * temp elements, e.g., col.get(i), col.pop(). * * @param cfg the method CFG whose disposal loops to be discovered */ - public void discoverDisposalLoops(ControlFlowGraph cfg) { + public void discoverDisposalLoopInfos(ControlFlowGraph cfg) { MethodTree methodTree = getEnclosingMethodTree(cfg.getUnderlyingAST()); if (methodTree == null) { return; } - preparedDisposalLoopsByMethod.put(methodTree, new LinkedHashSet<>(scanForDisposalLoops(cfg))); + preparedDisposalLoopInfosByMethod.put( + methodTree, new LinkedHashSet<>(scanForDisposalLoopInfos(cfg))); } /** - * Returns the {@link DisposalLoop}'s for the given underlying AST. + * Returns the {@link DisposalLoopInfo}'s for the given underlying AST. * * @param underlyingAST the underlying AST whose disposal loops should be returned * @return the set of disposal loops for {@code underlyingAST} */ - public Set getDisposalLoops(UnderlyingAST underlyingAST) { + public Set getDisposalLoopInfos(UnderlyingAST underlyingAST) { MethodTree methodTree = getEnclosingMethodTree(underlyingAST); if (methodTree == null) { return Collections.emptySet(); } - Set preparedDisposalLoops = preparedDisposalLoopsByMethod.get(methodTree); - if (preparedDisposalLoops == null) { + Set preparedDisposalLoopInfos = + preparedDisposalLoopInfosByMethod.get(methodTree); + if (preparedDisposalLoopInfos == null) { return Collections.emptySet(); } - return Collections.unmodifiableSet(new LinkedHashSet<>(preparedDisposalLoops)); + return Collections.unmodifiableSet(new LinkedHashSet<>(preparedDisposalLoopInfos)); } /** - * Removes and returns the {@link DisposalLoop}'s for the given underlying AST. + * Removes and returns the {@link DisposalLoopInfo}'s for the given underlying AST. * * @param underlyingAST the underlying AST whose disposal loops should be removed * @return the removed disposal loops for {@code underlyingAST} */ - private Set removePreparedDisposalLoops(UnderlyingAST underlyingAST) { + private Set removePreparedDisposalLoopInfos(UnderlyingAST underlyingAST) { MethodTree methodTree = getEnclosingMethodTree(underlyingAST); if (methodTree == null) { return Collections.emptySet(); } - Set preparedDisposalLoops = preparedDisposalLoopsByMethod.remove(methodTree); - if (preparedDisposalLoops == null) { + Set preparedDisposalLoopInfos = + preparedDisposalLoopInfosByMethod.remove(methodTree); + if (preparedDisposalLoopInfos == null) { return Collections.emptySet(); } - return preparedDisposalLoops; + return preparedDisposalLoopInfos; } /** @@ -342,18 +349,18 @@ protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { return; } - Set preparedDisposalLoops = removePreparedDisposalLoops(ast); - if (preparedDisposalLoops.isEmpty()) { + Set preparedDisposalLoopInfos = removePreparedDisposalLoopInfos(ast); + if (preparedDisposalLoopInfos.isEmpty()) { return; } MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(ResourceLeakUtils.getResourceLeakChecker(this), true); - for (DisposalLoop disposalLoop : preparedDisposalLoops) { + for (DisposalLoopInfo disposalLoopInfo : preparedDisposalLoopInfos) { Set calledMethods = - mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoop); + mustCallConsistencyAnalyzer.analyzeDisposalLoop(cfg, disposalLoopInfo); if (calledMethods != null) { - registerCalledMethods(disposalLoop, calledMethods); + registerCalledMethods(disposalLoopInfo, calledMethods); } } } @@ -486,6 +493,9 @@ public boolean isOwningCollectionField(Element elt) { * @return true if the element is a resource collection field */ public boolean isResourceCollectionField(Element elt) { + if (elt == null) { + return false; + } if (elt.getKind().isField()) { return isResourceCollection(elt.asType()); } @@ -813,7 +823,7 @@ public List getCollectionFieldDestructorAnnoFields(ExecutableElement met } /** - * Returnst true if the given expression {@code e} refers to {@code this.field}. + * Returns true if the given expression {@code e} refers to {@code this.field}. * * @param e the expression * @param field the field diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 98f5e1043ccc..6436762ac1a4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -34,9 +34,15 @@ import org.checkerframework.javacutil.TypesUtils; /** - * Transfer function for the collection ownership type system. Its primary purpose is to create - * temporary variables for expressions (which allow those expressions to have refined information in - * the store, which the consistency checker can use). + * Transfer function for the collection ownership type system. + * + *

This class implements the transfer rules for collection ownership. In particular, it handles + * ownership transfer through assignments, method invocations, and object creation. It refines + * collections to {@code @OwningCollectionWithoutObligation} when a disposal loop discharges the + * required must-call obligations of the iterated elements. + * + *

This class also creates temporary variables for expressions so their collection ownership + * types can be refined in the store and later queried by the consistency checker. */ public class CollectionOwnershipTransfer extends CFAbstractTransfer { @@ -91,6 +97,9 @@ public TransferResult visitAssignment( TreeUtils.elementFromTree(node.getExpression().getTree()))) { replaceInStores(res, lhsJE, atypeFactory.NOTOWNINGCOLLECTION); } else { + // Make the RHS @NotOwningCollection to reflect the ownership transfer, unless there is + // a @NotOwningCollection annotation, in which case there is no transfer and the lhs + // should be @NotOwningCollection replaceInStores( res, hasExplicitNotOwningCollectionDeclaration(node) ? lhsJE : rhsJE, @@ -114,12 +123,12 @@ public TransferResult visitAssignment( */ private TransferResult updateStoreForDisposalLoop( TransferResult res, Tree tree) { - DisposalLoop disposalLoop = atypeFactory.getDisposalLoopForConditionTree(tree); - if (disposalLoop != null) { + DisposalLoopInfo disposalLoopInfo = atypeFactory.getDisposalLoopInfoForConditionTree(tree); + if (disposalLoopInfo != null) { CollectionOwnershipStore elseStore = res.getElseStore(); - ExpressionTree collectionExpression = disposalLoop.expressionTree; + ExpressionTree collectionExpression = disposalLoopInfo.expressionTree(); JavaExpression collectionJE = JavaExpression.fromTree(collectionExpression); - Set disposalLoopCalledMethods = atypeFactory.getCalledMethods(disposalLoop); + Set disposalLoopCalledMethods = atypeFactory.getCalledMethods(disposalLoopInfo); CollectionOwnershipType collectionCoType = atypeFactory.getCoType(collectionExpression); if (collectionCoType == CollectionOwnershipType.OwningCollection) { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java index 959a82b65a87..64ab76ae080a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.collectionownership; -import com.sun.source.tree.BlockTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MemberSelectTree; @@ -9,10 +8,8 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import java.util.ArrayDeque; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; import javax.lang.model.element.Element; @@ -28,7 +25,7 @@ import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.IPair; -/** Utility methods shared by {@link DisposalLoop} scanning and AST matching. */ +/** Utility methods shared by {@link DisposalLoopInfo} scanning and AST matching. */ public final class CollectionOwnershipUtils { /** Do not instantiate */ @@ -36,27 +33,6 @@ private CollectionOwnershipUtils() { throw new BugInCF("CollectionOwnershipUtils is a utility class and should not be instantiated"); } - /** - * Returns the given statement as a list of statements. - * - *

If {@code statement} is a {@link BlockTree}, returns that block's statements. Otherwise, - * returns a singleton list containing {@code statement}. Returns {@code null} if {@code - * statement} is {@code null}. - * - * @param statement a loop-body statement - * @return a list of statements for {@code statement}, or {@code null} if {@code statement} is - * {@code null} - */ - static @Nullable List asStatementList( - @Nullable StatementTree statement) { - if (statement == null) { - return null; - } - return statement instanceof BlockTree blockTree - ? blockTree.getStatements() - : Collections.singletonList(statement); - } - /** * Returns the first CFG block associated with the given tree. * @@ -116,7 +92,7 @@ static Tree cfgAssociatedTreeFor(ControlFlowGraph cfg, Tree tree) { * @param expr an expression * @return the name of the referenced identifier, or {@code null} if none */ - static Name getNameFromExpressionTree(ExpressionTree expr) { + static @Nullable Name getNameFromExpressionTree(@Nullable ExpressionTree expr) { if (expr == null) { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java deleted file mode 100644 index 9f64884ff310..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoop.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.checkerframework.checker.collectionownership; - -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.Tree; -import java.util.Objects; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.node.Node; - -/** - * Stores the resolved CFG and AST facts for a disposal loop. A disposal loop is a loop that - * iterates over a resource collection and may call the disposal method, e.g., close() on the - * iterated resource. - */ -public class DisposalLoop { - - /** The {@code ExpressionTree} for collection that this loop iterates over. */ - public final ExpressionTree expressionTree; - - /** The {@code Tree} for the iterated collection element by this loop. */ - public final Tree iteratedElementTree; - - /** The CFG {@code Node} for the iterated collection element by this loop. */ - public final Node iteratedElementNode; - - /** The condition {@code Tree} for this loop. */ - public final Tree loopConditionTree; - - /** The conditional {@code Block} corresponding to the loop condition. */ - public final ConditionalBlock loopConditionalBlock; - - /** The entry {@code Block} for this loop's body. */ - public final Block loopBodyEntryBlock; - - /** The loop-update {@code Block}. */ - public final Block loopUpdateBlock; - - /** - * Constructs a new {@code DisposalLoop}. - * - * @param collectionExpressionTree the {@code ExpressionTree} for the collection that this loop - * iterates over - * @param iteratedElementTree the {@code Tree} for the iterated collection element - * @param iteratedElementNode the CFG {@code Node} for the iterated collection element - * @param loopConditionTree the condition {@code Tree} for this loop - * @param loopConditionBlock the conditional {@code Block} corresponding to the loop condition - * @param loopBodyEntryBlock the entry {@code Block} for this loop's body - * @param loopUpdateBlock the loop-update {@code Block} - */ - public DisposalLoop( - ExpressionTree collectionExpressionTree, - Tree iteratedElementTree, - Node iteratedElementNode, - Tree loopConditionTree, - ConditionalBlock loopConditionBlock, - Block loopBodyEntryBlock, - Block loopUpdateBlock) { - this.expressionTree = Objects.requireNonNull(collectionExpressionTree); - this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); - this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); - this.loopConditionTree = Objects.requireNonNull(loopConditionTree); - this.loopConditionalBlock = Objects.requireNonNull(loopConditionBlock); - this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); - this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java new file mode 100644 index 000000000000..94603327789f --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java @@ -0,0 +1,61 @@ +package org.checkerframework.checker.collectionownership; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.Tree; +import java.util.Objects; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.node.Node; + +/** + * Stores the resolved CFG and AST facts for a potential disposal loop. A disposal loop is a loop + * that iterates over a resource collection and may call the disposal method, e.g., close() on the + * iterated resource. This record stores the metadata for disposal loops which may or may not + * fullfill the must-call obligations of the iterated element. + * + * @param expressionTree The {@code ExpressionTree} for collection that this loop iterates over. + * @param iteratedElementTree The {@code Tree} for the iterated collection element by this loop. + * @param iteratedElementNode The CFG {@code Node} for the iterated collection element by this loop. + * @param loopConditionTree The condition {@code Tree} for this loop. + * @param loopConditionalBlock The conditional {@code Block} corresponding to the loop condition. + * @param loopBodyEntryBlock The entry {@code Block} for this loop's body. + * @param loopUpdateBlock The loop-update {@code Block}. + */ +public record DisposalLoopInfo( + ExpressionTree expressionTree, + Tree iteratedElementTree, + Node iteratedElementNode, + Tree loopConditionTree, + ConditionalBlock loopConditionalBlock, + Block loopBodyEntryBlock, + Block loopUpdateBlock) { + + /** + * Constructs a new {@code DisposalLoopInfo}. + * + * @param expressionTree the {@code ExpressionTree} for the collection that this loop iterates + * over + * @param iteratedElementTree the {@code Tree} for the iterated collection element + * @param iteratedElementNode the CFG {@code Node} for the iterated collection element + * @param loopConditionTree the condition {@code Tree} for this loop + * @param loopConditionalBlock the conditional {@code Block} corresponding to the loop condition + * @param loopBodyEntryBlock the entry {@code Block} for this loop's body + * @param loopUpdateBlock the loop-update {@code Block} + */ + public DisposalLoopInfo( + ExpressionTree expressionTree, + Tree iteratedElementTree, + Node iteratedElementNode, + Tree loopConditionTree, + ConditionalBlock loopConditionalBlock, + Block loopBodyEntryBlock, + Block loopUpdateBlock) { + this.expressionTree = Objects.requireNonNull(expressionTree); + this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); + this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); + this.loopConditionTree = Objects.requireNonNull(loopConditionTree); + this.loopConditionalBlock = Objects.requireNonNull(loopConditionalBlock); + this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); + this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java index 62f2adaa5f7d..5d45f15518d4 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java @@ -12,7 +12,7 @@ import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.dataflow.cfg.ControlFlowGraph; -/** Scans one method tree and discovers {@link DisposalLoop}'s in its CFG. */ +/** Scans one method tree and discovers {@link DisposalLoopInfo}'s in it. */ public class DisposalLoopScanner extends TreeScanner { /** The CO type factory used for collection-ownership queries. */ @@ -25,7 +25,7 @@ public class DisposalLoopScanner extends TreeScanner { private final ControlFlowGraph cfg; /** Disposal loops discovered while scanning the current method tree. */ - private final Set disposalLoops = new LinkedHashSet<>(); + private final Set disposalLoopInfos = new LinkedHashSet<>(); /** Matcher for indexed `for` disposal loops. */ private final IndexedForDisposalLoopMatcher indexedForDisposalLoopMatcher; @@ -63,10 +63,9 @@ public DisposalLoopScanner( * @param tree the tree to scan * @return the disposal loops discovered in {@code tree} */ - public Set scanTree(Tree tree) { - disposalLoops.clear(); + public Set scanTree(Tree tree) { scan(tree, null); - return new LinkedHashSet<>(disposalLoops); + return disposalLoopInfos; } /** @@ -104,16 +103,17 @@ public Void visitClass(ClassTree tree, Void p) { public Void visitForLoop(ForLoopTree tree, Void p) { boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; if (singleLoopVariable) { - DisposalLoop disposalLoop = indexedForDisposalLoopMatcher.match(tree); - if (disposalLoop != null) { - disposalLoops.add(disposalLoop); + DisposalLoopInfo disposalLoopInfo = indexedForDisposalLoopMatcher.match(tree); + if (disposalLoopInfo != null) { + disposalLoopInfos.add(disposalLoopInfo); } } return super.visitForLoop(tree, p); } /** - * Matches a {@link DisposalLoop} that uses while-loops and resolves their CFG-local loop facts. + * Matches a {@link DisposalLoopInfo} that uses while-loops and resolves their CFG-local loop + * facts. * * @param tree the while-loop to inspect * @param p the scan parameter @@ -121,9 +121,9 @@ public Void visitForLoop(ForLoopTree tree, Void p) { */ @Override public Void visitWhileLoop(WhileLoopTree tree, Void p) { - DisposalLoop disposalLoop = whileDisposalLoopMatcher.match(tree); - if (disposalLoop != null) { - disposalLoops.add(disposalLoop); + DisposalLoopInfo disposalLoopInfo = whileDisposalLoopMatcher.match(tree); + if (disposalLoopInfo != null) { + disposalLoopInfos.add(disposalLoopInfo); } return super.visitWhileLoop(tree, p); } @@ -138,9 +138,9 @@ public Void visitWhileLoop(WhileLoopTree tree, Void p) { */ @Override public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { - DisposalLoop disposalLoop = enhancedForDisposalLoopResolver.match(tree); - if (disposalLoop != null) { - disposalLoops.add(disposalLoop); + DisposalLoopInfo disposalLoopInfo = enhancedForDisposalLoopResolver.match(tree); + if (disposalLoopInfo != null) { + disposalLoopInfos.add(disposalLoopInfo); } return super.visitEnhancedForLoop(tree, p); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java index 4323309c8170..2f1a52898b6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -23,7 +23,7 @@ import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.IPair; -/** Resolves enhanced-`for` {@link DisposalLoop} from CFG. */ +/** Resolves enhanced-`for` {@link DisposalLoopInfo} from CFG. */ final class EnhancedForDisposalLoopResolver { /** The CO type factory used for collection-ownership queries. */ @@ -45,13 +45,13 @@ final class EnhancedForDisposalLoopResolver { } /** - * Returns the {@link DisposalLoop} along with its facts if the enhanced-for-loop iterates over a - * resource collection. + * Returns the {@link DisposalLoopInfo} if the enhanced-for-loop iterates over a resource + * collection. * * @param tree the enhanced-for-loop to inspect - * @return the matched disposal loop, or {@code null} if the loop does not match + * @return the matched disposal loop info, or {@code null} if the loop does not match */ - @Nullable DisposalLoop match(EnhancedForLoopTree tree) { + @Nullable DisposalLoopInfo match(EnhancedForLoopTree tree) { ExpressionTree collectionTree = CollectionOwnershipUtils.baseExpression(tree.getExpression()); if (collectionTree == null) { return null; @@ -63,12 +63,16 @@ final class EnhancedForDisposalLoopResolver { } /** - * Resolves an enhanced-for-loop candidate into a {@link DisposalLoop}. + * Searches the method CFG for the first occurrence of {@code tree} and resolves it to {@link + * DisposalLoopInfo}. + * + *

This performs a CFG traversal, looking for a desugared iterator {@code hasNext()} node + * tagged with the target enhanced-for-loop. * * @param tree the enhanced-for-loop to resolve - * @return the CFG-resolved loop, or {@code null} if it cannot be resolved + * @return the CFG-resolved loop info, or {@code null} if it cannot be resolved */ - private @Nullable DisposalLoop resolveEnhancedForLoop(EnhancedForLoopTree tree) { + private @Nullable DisposalLoopInfo resolveEnhancedForLoop(EnhancedForLoopTree tree) { Block entryBlock = cfg.getEntryBlock(); Set visitedBlocks = new HashSet<>(); Deque worklist = new ArrayDeque<>(); @@ -77,16 +81,19 @@ final class EnhancedForDisposalLoopResolver { while (!worklist.isEmpty()) { Block currentBlock = worklist.removeFirst(); - for (Node node : currentBlock.getNodes()) { if (node instanceof MethodInvocationNode methodInvocationNode) { - DisposalLoop resolvedLoop = resolveEnhancedForLoop(methodInvocationNode, tree); + DisposalLoopInfo resolvedLoop = resolveEnhancedForLoop(methodInvocationNode, tree); if (resolvedLoop != null) { return resolvedLoop; } } } + // One AST enhanced-for can have multiple CFG occurrences (for example around duplicated + // finally paths for normal and exceptional flow). This resolver returns the first matching + // occurrence it finds, so avoid traversing ignored exceptional successors here; otherwise + // the search can bind to an exceptional clone before the normal one. for (IPair successorAndExceptionType : CollectionOwnershipUtils.getSuccessorsExceptIgnoredExceptions(currentBlock, coAtf)) { Block successorBlock = successorAndExceptionType.first; @@ -100,55 +107,61 @@ final class EnhancedForDisposalLoopResolver { } /** - * Returns a resolved collection loop if the given node is desugared from an enhanced-for-loop - * over a resource collection. + * Returns resolved disposal loop info if the given node is desugared from the target + * enhanced-for-loop {@code tree} over a resource collection. + * + *

Starting from the desugared iterator {@code hasNext()} node, this walks forward to recover + * the loop-variable assignment and body-entry block, then walks backward to recover the loop + * condition and update block. * * @param methodInvocationNode the node to check * @param tree the enhanced-for-loop being resolved - * @return the resolved loop, or {@code null} if the node does not belong to {@code tree} + * @return the resolved loop info, or {@code null} if the node does not belong to {@code tree} */ - private @Nullable DisposalLoop resolveEnhancedForLoop( + private @Nullable DisposalLoopInfo resolveEnhancedForLoop( MethodInvocationNode methodInvocationNode, @FindDistinct EnhancedForLoopTree tree) { - if (methodInvocationNode.getIterableExpression() == null) { + if (!isTargetEnhancedForInvocation(methodInvocationNode, tree)) { return null; } - EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); - if (loop == null) { - throw new BugInCF( - "MethodInvocationNode.iterableExpression should be non-null iff" - + " MethodInvocationNode.enhancedForLoop is non-null"); - } - if (loop != tree) { + VariableTree loopVariable = tree.getVariable(); + + // Walk forward from the iterator `hasNext()` node to find the assignment that initializes the + // loop variable from `next()`. The successor after that assignment is the loop-body entry + // block. + Block blockContainingHasNext = methodInvocationNode.getBlock(); + if (!(blockContainingHasNext instanceof SingleSuccessorBlock singleSuccessorBlock)) { return null; } - - VariableTree loopVariable = loop.getVariable(); - - SingleSuccessorBlock singleSuccessorBlock = - (SingleSuccessorBlock) methodInvocationNode.getBlock(); Iterator nodeIterator = singleSuccessorBlock.getNodes().iterator(); Node loopVariableNode = null; - Node node; + Node candidateNode; boolean isAssignmentOfLoopVariable; do { while (!nodeIterator.hasNext()) { - singleSuccessorBlock = (SingleSuccessorBlock) singleSuccessorBlock.getSuccessor(); + Block successor = singleSuccessorBlock.getSuccessor(); + if (!(successor instanceof SingleSuccessorBlock nextBlock)) { + return null; + } + singleSuccessorBlock = nextBlock; nodeIterator = singleSuccessorBlock.getNodes().iterator(); } - node = nodeIterator.next(); + candidateNode = nodeIterator.next(); isAssignmentOfLoopVariable = false; - if ((node instanceof AssignmentNode) - && (node.getTree() instanceof VariableTree iteratorVariableDeclaration)) { - loopVariableNode = ((AssignmentNode) node).getTarget(); + if ((candidateNode instanceof AssignmentNode) + && (candidateNode.getTree() instanceof VariableTree iteratorVariableDeclaration)) { + loopVariableNode = ((AssignmentNode) candidateNode).getTarget(); isAssignmentOfLoopVariable = - iteratorVariableDeclaration.getName() == loopVariable.getName(); + iteratorVariableDeclaration.getName().equals(loopVariable.getName()); } } while (!isAssignmentOfLoopVariable); Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); + // Walk backward from the iterator `hasNext()` node to recover the loop-condition node and the + // loop-update/back-edge block for this desugared enhanced-for. Block loopUpdateBlock = methodInvocationNode.getBlock(); nodeIterator = loopUpdateBlock.getNodes().iterator(); + Node loopConditionNode; boolean isLoopCondition; do { while (!nodeIterator.hasNext()) { @@ -160,16 +173,17 @@ final class EnhancedForDisposalLoopResolver { return null; } } - node = nodeIterator.next(); + loopConditionNode = nodeIterator.next(); isLoopCondition = false; - if (node instanceof MethodInvocationNode) { - MethodInvocationTree methodInvocationTree = ((MethodInvocationNode) node).getTree(); + if (loopConditionNode instanceof MethodInvocationNode) { + MethodInvocationTree methodInvocationTree = + ((MethodInvocationNode) loopConditionNode).getTree(); isLoopCondition = methodInvocationTree != null && TreeUtils.isHasNextCall(methodInvocationTree); } } while (!isLoopCondition); - Block blockContainingLoopCondition = node.getBlock(); + Block blockContainingLoopCondition = loopConditionNode.getBlock(); if (blockContainingLoopCondition.getSuccessors().size() != 1) { throw new BugInCF( "loop condition has: " @@ -182,17 +196,43 @@ final class EnhancedForDisposalLoopResolver { "loop condition successor is not ConditionalBlock, but: " + maybeConditionalBlock.getClass()); } - if (loopVariableNode == null || loopVariableNode.getTree() == null || node.getTree() == null) { + if (loopVariableNode == null + || loopVariableNode.getTree() == null + || loopConditionNode.getTree() == null) { return null; } - return new DisposalLoop( - loop.getExpression(), + return new DisposalLoopInfo( + tree.getExpression(), loopVariableNode.getTree(), loopVariableNode, - node.getTree(), + loopConditionNode.getTree(), conditionalBlock, loopBodyEntryBlock, loopUpdateBlock); } + + /** + * Returns whether {@code methodInvocationNode} is the iterator {@code hasNext()} invocation for + * the target enhanced-for-loop {@code tree}. + * + * @param methodInvocationNode the node to check + * @param tree the target enhanced-for-loop + * @return true if the node belongs to {@code tree} + */ + private boolean isTargetEnhancedForInvocation( + MethodInvocationNode methodInvocationNode, @FindDistinct EnhancedForLoopTree tree) { + if (methodInvocationNode.getIterableExpression() == null) { + return false; + } + + EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); + if (loop == null) { + throw new BugInCF( + "MethodInvocationNode.iterableExpression should be non-null iff" + + " MethodInvocationNode.enhancedForLoop is non-null"); + } + + return loop == tree; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index 500d3375d0a8..bb1db9c326c7 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -2,6 +2,7 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.BlockTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; @@ -26,7 +27,7 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches indexed `for` {@link DisposalLoop}'s that iterates over a resource collection. */ +/** Matches indexed `for` {@link DisposalLoopInfo}'s that iterates over a resource collection. */ final class IndexedForDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ @@ -48,20 +49,27 @@ final class IndexedForDisposalLoopMatcher { } /** - * Returns the {@link DisposalLoop} if the `for` loop iterates over a resource collection and + * Returns the {@link DisposalLoopInfo} if the `for` loop iterates over a resource collection and * follows a specific loop shape described in {@link #nameOfCollectionThatAllElementsAreCalledOn}. * * @param tree a `for` loop with exactly one loop variable * @return the matched disposal loop, or {@code null} if the loop does not match */ - @Nullable DisposalLoop match(ForLoopTree tree) { + @Nullable DisposalLoopInfo match(ForLoopTree tree) { + StatementTree loopBodyStatement = tree.getStatement(); List loopBodyStatements = - CollectionOwnershipUtils.asStatementList(tree.getStatement()); + loopBodyStatement instanceof BlockTree blockTree + ? blockTree.getStatements() + : List.of(loopBodyStatement); if (loopBodyStatements == null) { return null; } StatementTree init = tree.getInitializer().get(0); - ExpressionTree condition = TreeUtils.withoutParens(tree.getCondition()); + ExpressionTree rawCondition = tree.getCondition(); + if (rawCondition == null) { + return null; + } + ExpressionTree condition = TreeUtils.withoutParens(rawCondition); ExpressionStatementTree update = tree.getUpdate().get(0); if (!(condition instanceof BinaryTree binaryTreeCondition)) { return null; @@ -85,7 +93,7 @@ final class IndexedForDisposalLoopMatcher { } Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); - return new DisposalLoop( + return new DisposalLoopInfo( CollectionOwnershipUtils.baseExpression(collectionElementTree), collectionElementTree, nodeForCollectionElt, diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index 684f857237bc..00f714b98abe 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -2,6 +2,7 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.BlockTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LiteralTree; @@ -37,7 +38,7 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches `while` {@link DisposalLoop} that iterates over a resource collection. */ +/** Matches `while` {@link DisposalLoopInfo} that iterates over a resource collection. */ final class WhileDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ @@ -343,7 +344,8 @@ private static final class BodyExtraction { } /** - * Matches a {@link DisposalLoop} that uses a while-loop and resolves its CFG-local loop facts. + * Matches a {@link DisposalLoopInfo} that uses a while-loop and resolves its CFG-local loop + * facts. * *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, @@ -352,14 +354,17 @@ private static final class BodyExtraction { * @param tree the while-loop to inspect * @return the matched disposal loop, or {@code null} if the loop does not match */ - @Nullable DisposalLoop match(WhileLoopTree tree) { + @Nullable DisposalLoopInfo match(WhileLoopTree tree) { ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); WhileHeaderMatch header = matchWhileHeader(condNoParens); if (header == null) { return null; } + StatementTree loopBodyStatement = tree.getStatement(); List bodyStatements = - CollectionOwnershipUtils.asStatementList(tree.getStatement()); + loopBodyStatement instanceof BlockTree blockTree + ? blockTree.getStatements() + : List.of(loopBodyStatement); if (bodyStatements == null) { return null; } @@ -397,7 +402,7 @@ private static final class BodyExtraction { chooseLoopUpdateBlockForPotentiallyFulfillingLoop( loopBodyEntryBlock, cblock, getOrCreateWhileLoopCache()); if (loopUpdateBlock != null) { - return new DisposalLoop( + return new DisposalLoopInfo( header.collectionTree, extraction.extractionCall, elementNode, diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 4d4d449e9354..2e9dd98c461a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -44,7 +44,7 @@ import org.checkerframework.checker.collectionownership.CollectionOwnershipAnnotatedTypeFactory.CollectionOwnershipType; import org.checkerframework.checker.collectionownership.CollectionOwnershipStore; import org.checkerframework.checker.collectionownership.CollectionOwnershipUtils; -import org.checkerframework.checker.collectionownership.DisposalLoop; +import org.checkerframework.checker.collectionownership.DisposalLoopInfo; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -2750,12 +2750,12 @@ private void propagateObligationsToSuccessorBlock( // loop which fulfills the collections must-call obligation. If so, don't propagate the // collection obligations discharged inside the loop. boolean isElseEdgeOfDisposalLoop = false; - DisposalLoop disposalLoop = coAtf.getDisposalLoopForConditionBlock(currentBlock); + DisposalLoopInfo disposalLoopInfo = coAtf.getDisposalLoopInfoForConditionBlock(currentBlock); Set disposalLoopCalledMethods = null; - if ((currentBlock instanceof ConditionalBlock conditionalBlock) && disposalLoop != null) { + if ((currentBlock instanceof ConditionalBlock conditionalBlock) && disposalLoopInfo != null) { if (conditionalBlock.getElseSuccessor().equals(successor)) { isElseEdgeOfDisposalLoop = true; - disposalLoopCalledMethods = coAtf.getCalledMethods(disposalLoop); + disposalLoopCalledMethods = coAtf.getCalledMethods(disposalLoopInfo); } } @@ -3464,33 +3464,33 @@ public static String collectionToString(Collection bwos) { } /** - * Analyze the loop body of a {@link DisposalLoop} to compute the definitely called-methods on the - * iterated element on every path. + * Analyze the loop body of a {@link DisposalLoopInfo} to compute the definitely called-methods on + * the iterated element on every path. * * @param cfg the cfg of the enclosing method - * @param disposalLoop the loop to analyze + * @param disposalLoopInfo the loop to analyze * @return the called-methods for the loop on the iterated element, or {@code null} if there's no * definite called-methods on the iterated element of the loop */ - public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoop disposalLoop) { + public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoopInfo disposalLoopInfo) { // ensure checked loop is initialized in a valid way Objects.requireNonNull( - disposalLoop.iteratedElementTree, + disposalLoopInfo.iteratedElementTree(), "CollectionElementAccess tree provided to analyze loop body of an" + " CFG-resolved potentially fulfilling collection loop is null."); Objects.requireNonNull( - disposalLoop.loopBodyEntryBlock, + disposalLoopInfo.loopBodyEntryBlock(), "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + " loop is null."); Objects.requireNonNull( - disposalLoop.loopUpdateBlock, + disposalLoopInfo.loopUpdateBlock(), "Block provided to analyze loop body of a CFG-resolved potentially fulfilling collection" + " loop is null."); - Block loopBodyEntryBlock = disposalLoop.loopBodyEntryBlock; - Block loopUpdateBlock = disposalLoop.loopUpdateBlock; - Tree collectionElement = disposalLoop.iteratedElementTree; + Block loopBodyEntryBlock = disposalLoopInfo.loopBodyEntryBlock(); + Block loopUpdateBlock = disposalLoopInfo.loopUpdateBlock(); + Tree collectionElement = disposalLoopInfo.iteratedElementTree(); boolean emptyLoopBody = loopBodyEntryBlock.equals(loopUpdateBlock); if (emptyLoopBody) { @@ -3547,7 +3547,7 @@ public Set analyzeDisposalLoop(ControlFlowGraph cfg, DisposalLoop dispos if (isLastBlockOfBody) { Set calledMethodsAfterBlock = analyzeTypeOfCollectionElement( - currentBlock, disposalLoop, obligations, loopUpdateBlock); + currentBlock, disposalLoopInfo, obligations, loopUpdateBlock); // intersect the called methods after this block with the accumulated ones so far. // This is required because there may be multiple "back edges" of the loop, in which // case we must intersect the called methods between those. @@ -3668,7 +3668,7 @@ private Set computeLoopRegion(Block entry, Block update) { * aliases. * * @param lastLoopBodyBlock last block of loop body - * @param disposalLoop loop wrapper of the loop to analyze + * @param disposalLoopInfo loop wrapper of the loop to analyze * @param obligations the set of tracked obligations * @param loopUpdateBlock block that updates the loop * @return the union of methods in the CalledMethods type of the collection element and all its @@ -3676,7 +3676,7 @@ private Set computeLoopRegion(Block entry, Block update) { */ private Set analyzeTypeOfCollectionElement( Block lastLoopBodyBlock, - DisposalLoop disposalLoop, + DisposalLoopInfo disposalLoopInfo, Set obligations, Block loopUpdateBlock) { AccumulationStore store = null; @@ -3692,7 +3692,7 @@ private Set analyzeTypeOfCollectionElement( store = cmAtf.getStoreAfter(lastLoopBodyBlock.getLastNode()); } Obligation collectionElementObligation = - getObligationForVar(obligations, disposalLoop.iteratedElementTree); + getObligationForVar(obligations, disposalLoopInfo.iteratedElementTree()); if (collectionElementObligation == null) { // the loop did something weird. Might have reassigned the collection element. // The sound thing to do is return an empty list. @@ -3708,7 +3708,7 @@ private Set analyzeTypeOfCollectionElement( // add the called methods of the ICE IteratedCollectionElement ice = store.getIteratedCollectionElement( - disposalLoop.iteratedElementNode, disposalLoop.iteratedElementTree); + disposalLoopInfo.iteratedElementNode(), disposalLoopInfo.iteratedElementTree()); if (ice != null) { AccumulationValue cmValOfIce = store.getValue(ice); List calledMethods = getCalledMethods(cmValOfIce); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index e2782d9e946e..414d343ac96e 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -5,7 +5,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -298,8 +297,7 @@ public static boolean isCollection(TypeMirror type) { return false; } return Iterable.class.isAssignableFrom(elementRawType) - || Iterator.class.isAssignableFrom(elementRawType) - || Map.class.isAssignableFrom(elementRawType); + || Iterator.class.isAssignableFrom(elementRawType); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index a7f36f05f087..6bee059796ae 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -187,7 +187,8 @@ protected void postCFGConstruction(ControlFlowGraph cfg, UnderlyingAST ast) { // col.get(i) in order for the called-method analysis to properly track must-call obligations // on them. if (ast.getKind() == UnderlyingAST.Kind.METHOD) { - ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this).discoverDisposalLoops(cfg); + ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(this) + .discoverDisposalLoopInfos(cfg); } } diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java index 0917f59f7842..05377c8d6911 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsTransfer.java @@ -9,7 +9,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.CalledMethodsTransfer; -import org.checkerframework.checker.collectionownership.DisposalLoop; +import org.checkerframework.checker.collectionownership.DisposalLoopInfo; import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; @@ -62,19 +62,19 @@ public void accumulate( } /** - * Add the collection elements iterated over in {@link DisposalLoop}'s to the store so that + * Add the collection elements iterated over in {@link DisposalLoopInfo}'s to the store so that * temp-vars. e.g., col.get(i), col.pop() are tracked. */ @Override public AccumulationStore initialStore( UnderlyingAST underlyingAST, List parameters) { AccumulationStore store = super.initialStore(underlyingAST, parameters); - for (DisposalLoop disposalLoop : + for (DisposalLoopInfo disposalLoopInfo : ResourceLeakUtils.getCollectionOwnershipAnnotatedTypeFactory(rlTypeFactory) - .getDisposalLoops(underlyingAST)) { + .getDisposalLoopInfos(underlyingAST)) { IteratedCollectionElement collectionElementJE = new IteratedCollectionElement( - disposalLoop.iteratedElementNode, disposalLoop.iteratedElementTree); + disposalLoopInfo.iteratedElementNode(), disposalLoopInfo.iteratedElementTree()); store.insertValue(collectionElementJE, rlTypeFactory.top); } return store; diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index 15c5600fdd1f..c2a85a868bff 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -263,9 +263,9 @@ public void remove() { /** * Advances the reader by reading a single line, updating the {@code nextLine} and {@code - * NextLineNumber} fields. If there is no remaining line, the reader is {@code close}d. + * nextLineNumber} fields. If there is no remaining line, the reader is {@code close}d. * - * @throws IOException propagated reader exception + * @throws IOException if reading from the underlying reader fails */ @RequiresNonNull("reader") protected void advance(@NotOwningCollection @UnknownInitialization JavaDiagnosticReader this) From ef2595ddd73103c579b27ba046cb3e793202bf96 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 7 May 2026 01:50:56 -0700 Subject: [PATCH 368/374] equals -> == --- .../collectionownership/EnhancedForDisposalLoopResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java index 2f1a52898b6a..22829231ebff 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -152,7 +152,7 @@ final class EnhancedForDisposalLoopResolver { && (candidateNode.getTree() instanceof VariableTree iteratorVariableDeclaration)) { loopVariableNode = ((AssignmentNode) candidateNode).getTarget(); isAssignmentOfLoopVariable = - iteratorVariableDeclaration.getName().equals(loopVariable.getName()); + iteratorVariableDeclaration.getName() == loopVariable.getName(); } } while (!isAssignmentOfLoopVariable); Block loopBodyEntryBlock = singleSuccessorBlock.getSuccessor(); From c2ecad36ee97038a79084c07c7c1cbf17d40acb7 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 7 May 2026 10:27:57 -0700 Subject: [PATCH 369/374] reverting to treating maps as resource collections. --- .../checker/resourceleak/ResourceLeakUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index 414d343ac96e..e2782d9e946e 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -297,7 +298,8 @@ public static boolean isCollection(TypeMirror type) { return false; } return Iterable.class.isAssignableFrom(elementRawType) - || Iterator.class.isAssignableFrom(elementRawType); + || Iterator.class.isAssignableFrom(elementRawType) + || Map.class.isAssignableFrom(elementRawType); } /** From ac65c407208b4d226b54eb8bafd4dd03bdeb171f Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 7 May 2026 15:49:34 -0700 Subject: [PATCH 370/374] Addresses review comment on the indexed for loop matcher. --- .../DisposalLoopScanner.java | 9 +- .../IndexedForDisposalLoopMatcher.java | 433 +++++++++++------- 2 files changed, 264 insertions(+), 178 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java index 5d45f15518d4..91c3d75cf431 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java @@ -101,12 +101,9 @@ public Void visitClass(ClassTree tree, Void p) { */ @Override public Void visitForLoop(ForLoopTree tree, Void p) { - boolean singleLoopVariable = tree.getUpdate().size() == 1 && tree.getInitializer().size() == 1; - if (singleLoopVariable) { - DisposalLoopInfo disposalLoopInfo = indexedForDisposalLoopMatcher.match(tree); - if (disposalLoopInfo != null) { - disposalLoopInfos.add(disposalLoopInfo); - } + DisposalLoopInfo disposalLoopInfo = indexedForDisposalLoopMatcher.match(tree); + if (disposalLoopInfo != null) { + disposalLoopInfos.add(disposalLoopInfo); } return super.visitForLoop(tree, p); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index bb1db9c326c7..d36882824e40 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -2,12 +2,13 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; @@ -16,8 +17,6 @@ import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreeScanner; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import javax.lang.model.element.Name; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -27,7 +26,7 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches indexed `for` {@link DisposalLoopInfo}'s that iterates over a resource collection. */ +/** Matches indexed `for` {@link DisposalLoopInfo}s that iterate over a resource collection. */ final class IndexedForDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ @@ -49,220 +48,310 @@ final class IndexedForDisposalLoopMatcher { } /** - * Returns the {@link DisposalLoopInfo} if the `for` loop iterates over a resource collection and - * follows a specific loop shape described in {@link #nameOfCollectionThatAllElementsAreCalledOn}. + * Returns the {@link DisposalLoopInfo} if the {@code for} loop {@code tree} iterates over a + * resource collection satisfying the following rules: * - * @param tree a `for` loop with exactly one loop variable - * @return the matched disposal loop, or {@code null} if the loop does not match + *

    + *
  • exactly one loop initializer statement and one update expression + *
  • initialization must be of the form {@code int i = 0} + *
  • condition must be of the form {@code i < collection.size()} + *
  • update must be prefix or postfix {@code ++} + *
  • no overwrite of the index variable or collection variable in the loop body + *
  • at least one access of the iterated collection element consistent with the loop header + * (e.g., {@code collection.get(i)}) in the loop body + *
+ * + *

For example: + * + *

{@code
+   * for (int i = 0; i < collection.size(); i++) {
+   *   Resource resource = collection.get(i);
+   *   resource.close();
+   * }
+   * }
+ * + * @param tree the `for` loop to inspect + * @return the matched disposal loop info, or {@code null} if the loop does not match */ @Nullable DisposalLoopInfo match(ForLoopTree tree) { - StatementTree loopBodyStatement = tree.getStatement(); - List loopBodyStatements = - loopBodyStatement instanceof BlockTree blockTree - ? blockTree.getStatements() - : List.of(loopBodyStatement); - if (loopBodyStatements == null) { + // Reject the loop if it doesn't have exactly one initializer and one update. + if (tree.getInitializer().size() != 1 || tree.getUpdate().size() != 1) { return null; } - StatementTree init = tree.getInitializer().get(0); + + StatementTree initializer = tree.getInitializer().get(0); + ExpressionStatementTree update = tree.getUpdate().get(0); ExpressionTree rawCondition = tree.getCondition(); if (rawCondition == null) { return null; } ExpressionTree condition = TreeUtils.withoutParens(rawCondition); - ExpressionStatementTree update = tree.getUpdate().get(0); if (!(condition instanceof BinaryTree binaryTreeCondition)) { return null; } - Name identifierInHeader = - nameOfCollectionThatAllElementsAreCalledOn(init, binaryTreeCondition, update); - Name iterator = CollectionOwnershipUtils.getNameFromStatementTree(init); - if (identifierInHeader == null || iterator == null) { + + Name indexVariableName = indexVariableNameIfCanonicalIndexLoop(initializer, update); + if (indexVariableName == null) { + return null; + } + + Name collectionName = + resourceCollectionNameIfConditionMatches(binaryTreeCondition, indexVariableName); + if (collectionName == null) { return null; } + + // Validate the loop body and recover the last `collection.get(i)` access that represents the + // iterated element for this loop. ExpressionTree collectionElementTree = - getLastElementAccessIfLoopValid(loopBodyStatements, identifierInHeader, iterator); - if (collectionElementTree != null) { - Block loopConditionBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, condition); - Block loopUpdateBlock = - CollectionOwnershipUtils.firstBlockForTree(cfg, update.getExpression()); - Node nodeForCollectionElt = - CollectionOwnershipUtils.anyNodeForTree(cfg, collectionElementTree); - if (loopUpdateBlock == null || loopConditionBlock == null || nodeForCollectionElt == null) { - return null; - } - Block conditionalBlock = ((SingleSuccessorBlock) loopConditionBlock).getSuccessor(); - Block loopBodyEntryBlock = ((ConditionalBlock) conditionalBlock).getThenSuccessor(); - return new DisposalLoopInfo( - CollectionOwnershipUtils.baseExpression(collectionElementTree), - collectionElementTree, - nodeForCollectionElt, - CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, condition), - (ConditionalBlock) conditionalBlock, - loopBodyEntryBlock, - loopUpdateBlock); - } - return null; + new LoopBodyScanner(collectionName, indexVariableName).scanLoopBody(tree.getStatement()); + if (collectionElementTree == null) { + return null; + } + + ExpressionTree collectionExpression = + CollectionOwnershipUtils.baseExpression(collectionElementTree); + if (collectionExpression == null) { + return null; + } + + // After the tree match succeeds, recover the CFG blocks corresponding to the loop condition, + // body entry, and loop update. + Block loopConditionBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, condition); + Block loopUpdateBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, update.getExpression()); + Node iteratedElementNode = CollectionOwnershipUtils.anyNodeForTree(cfg, collectionElementTree); + if (loopConditionBlock == null || loopUpdateBlock == null || iteratedElementNode == null) { + return null; + } + if (!(loopConditionBlock instanceof SingleSuccessorBlock singleSuccessorLoopConditionBlock)) { + return null; + } + Block conditionalBlockCandidate = singleSuccessorLoopConditionBlock.getSuccessor(); + if (!(conditionalBlockCandidate instanceof ConditionalBlock conditionalBlock)) { + return null; + } + // The then-successor is the body-entry block for the matching indexed loop. + Block loopBodyEntryBlock = conditionalBlock.getThenSuccessor(); + + return new DisposalLoopInfo( + collectionExpression, + collectionElementTree, + iteratedElementNode, + CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, condition), + conditionalBlock, + loopBodyEntryBlock, + loopUpdateBlock); } /** - * Conservatively decides whether a loop iterates over all elements of some collection, using the - * following rules: + * Returns true if the given tree is of the form {@code collection.get(i)}, where {@code i} is the + * given index variable name. * - *
    - *
  • only one loop variable - *
  • initialization must be of the form i = 0 - *
  • condition must be of the form (i < col.size()) - *
  • update must be prefix or postfix {@code ++} - *
- * - * Returns: + * @param tree the tree to check + * @param indexVariableName the index variable name + * @return true if the tree is of the form {@code collection.get(i)} + */ + private boolean isIthCollectionElement(Tree tree, Name indexVariableName) { + if (tree instanceof MethodInvocationTree methodInvocationTree + && indexVariableName + == CollectionOwnershipUtils.getNameFromExpressionTree( + TreeUtils.getIdxForGetCall(tree))) { + ExpressionTree methodSelect = methodInvocationTree.getMethodSelect(); + if (methodSelect instanceof MemberSelectTree memberSelectTree) { + return coAtf.isResourceCollection(memberSelectTree.getExpression()); + } + } + return false; + } + + /** + * Returns the index variable name if the initializer and update form a canonical zero-based index + * loop. * - *
    - *
  • null, if any of the above rules is violated - *
  • the name of the collection if the loop condition is of the form (i < col.size()) - *
+ *

This checks the {@code int i = 0} and {@code i++}/{@code ++i} parts of the loop header. * - * @param init the initializer of the loop - * @param condition the loop condition + * @param initializer the loop initializer * @param update the loop update - * @return the name of the collection that the loop iterates over all elements of, or null + * @return the index variable name, or {@code null} if the initializer/update do not match */ - Name nameOfCollectionThatAllElementsAreCalledOn( - StatementTree init, BinaryTree condition, ExpressionStatementTree update) { + private @Nullable Name indexVariableNameIfCanonicalIndexLoop( + StatementTree initializer, ExpressionStatementTree update) { Tree.Kind updateKind = update.getExpression().getKind(); - if (updateKind == Tree.Kind.PREFIX_INCREMENT || updateKind == Tree.Kind.POSTFIX_INCREMENT) { - UnaryTree inc = (UnaryTree) update.getExpression(); + if (updateKind != Tree.Kind.PREFIX_INCREMENT && updateKind != Tree.Kind.POSTFIX_INCREMENT) { + return null; + } - if (!(init instanceof VariableTree initVar) - || !(inc.getExpression() instanceof IdentifierTree)) return null; + if (!(initializer instanceof VariableTree initVariable)) { + return null; + } + if (!(update.getExpression() instanceof UnaryTree incrementExpression)) { + return null; + } + if (!(incrementExpression.getExpression() instanceof IdentifierTree updateIdentifier)) { + return null; + } - if (!(initVar.getInitializer() instanceof LiteralTree) - || !((LiteralTree) initVar.getInitializer()).getValue().equals(0)) { - return null; - } + ExpressionTree initializerValue = initVariable.getInitializer(); + if (!(initializerValue instanceof LiteralTree literalTree) + || !literalTree.getValue().equals(0)) { + return null; + } - if (!(condition.getLeftOperand() instanceof IdentifierTree)) { - return null; - } + Name indexVariableName = initVariable.getName(); + return indexVariableName == updateIdentifier.getName() ? indexVariableName : null; + } - Name initVarName = initVar.getName(); - if (initVarName != ((IdentifierTree) condition.getLeftOperand()).getName()) { - return null; - } - if (initVarName != ((IdentifierTree) inc.getExpression()).getName()) { - return null; - } + /** + * Returns the collection name if the loop condition is of the form {@code i < collection.size()} + * for the given index variable and the receiver {@code collection} is a resource collection. + * + * @param condition the loop condition + * @param indexVariableName the validated loop index variable + * @return the collection name, or {@code null} if the condition does not match + */ + private @Nullable Name resourceCollectionNameIfConditionMatches( + BinaryTree condition, Name indexVariableName) { + if (condition.getKind() != Tree.Kind.LESS_THAN) { + return null; + } + if (!(condition.getLeftOperand() instanceof IdentifierTree conditionIdentifier)) { + return null; + } + if (conditionIdentifier.getName() != indexVariableName) { + return null; + } + if (!(condition.getRightOperand() instanceof MethodInvocationTree) + || !TreeUtils.isSizeAccess(condition.getRightOperand())) { + return null; + } - if ((condition.getRightOperand() instanceof MethodInvocationTree) - && TreeUtils.isSizeAccess(condition.getRightOperand())) { - ExpressionTree methodSelect = - ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); - if (methodSelect instanceof MemberSelectTree mst) { - if (coAtf.isResourceCollection(mst.getExpression())) { - return CollectionOwnershipUtils.getNameFromExpressionTree(mst.getExpression()); - } - } - } + ExpressionTree methodSelect = + ((MethodInvocationTree) condition.getRightOperand()).getMethodSelect(); + if (!(methodSelect instanceof MemberSelectTree memberSelectTree)) { + return null; + } + if (!coAtf.isResourceCollection(memberSelectTree.getExpression())) { + return null; } - return null; + return CollectionOwnershipUtils.getNameFromExpressionTree(memberSelectTree.getExpression()); } /** - * Check that the loop does not contain any writes to the loop iterator variable or to the - * collection variable itself. Extract the collection access tree ({@code arr[i]} or {@code - * collection.get(i)} where {@code i} is the iterator variable and {@code collection/arr} is - * consistent with the loop header) and return the last encountered such tree. - * - * @param statements list of statements of the loop body - * @param identifierInHeader collection name if loop condition is {@code i < collection.size()} or - * {@code i < arr.length} and {@code n} if loop condition is {@code i < n} - * @param iterator the name of the loop iterator variable - * @return {@code null} if the loop body writes to the iterator or collection variable; otherwise - * the last collection element access tree consistent with the loop header, if one exists + * Scans an indexed {@code for} loop body, rejecting writes that invalidate the simple + * indexed-loop model and remembers the last matching {@code collection.get(i)} access. */ - private @Nullable ExpressionTree getLastElementAccessIfLoopValid( - List statements, Name identifierInHeader, Name iterator) { - AtomicBoolean blockIsIllegal = new AtomicBoolean(false); - final ExpressionTree[] collectionElementTree = {null}; - - TreeScanner scanner = - new TreeScanner() { - @Override - public Void visitUnary(UnaryTree tree, Void p) { - switch (tree.getKind()) { - case PREFIX_DECREMENT, POSTFIX_DECREMENT, PREFIX_INCREMENT, POSTFIX_INCREMENT -> { - if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getExpression()) - == iterator) { - blockIsIllegal.set(true); - } - } - default -> {} - } - return super.visitUnary(tree, p); - } + private final class LoopBodyScanner extends TreeScanner { - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { - if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()) - == iterator) { - blockIsIllegal.set(true); - } - return super.visitCompoundAssignment(tree, p); - } + /** The collection named in the loop header. */ + private final Name collectionName; - @Override - public Void visitAssignment(AssignmentTree tree, Void p) { - Name assignedVariable = - CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()); - if (assignedVariable == iterator || assignedVariable == identifierInHeader) { - blockIsIllegal.set(true); - } + /** The index variable named in the loop header. */ + private final Name indexVariableName; - return super.visitAssignment(tree, p); - } + /** Whether the loop body mutates the collection or index variable. */ + private boolean bodyIsIllegal = false; + + /** The last matching {@code collection.get(i)} access found in the body. */ + private @Nullable ExpressionTree lastCollectionElementAccess = null; + + /** + * Creates the {@link LoopBodyScanner}. + * + * @param collectionName the collection name from the loop header + * @param indexVariableName the index variable name from the loop header + */ + private LoopBodyScanner(Name collectionName, Name indexVariableName) { + this.collectionName = collectionName; + this.indexVariableName = indexVariableName; + } + + /** + * Scans the loop body once and returns the last matching element access if the body remains + * compatible with indexed disposal-loop matching. + * + * @param loopBody the loop body to scan + * @return the last matching {@code collection.get(i)} access, or {@code null} if the body + * writes to the collection or index variable, or if no matching element access is found + */ + private @Nullable ExpressionTree scanLoopBody(StatementTree loopBody) { + super.scan(loopBody, null); + return bodyIsIllegal ? null : lastCollectionElementAccess; + } - @Override - public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - if (isIthCollectionElement(mit, iterator) - && identifierInHeader == CollectionOwnershipUtils.getNameFromExpressionTree(mit) - && identifierInHeader != null) { - collectionElementTree[0] = mit; - } - return super.visitMethodInvocation(mit, p); + @Override + public Void visitUnary(UnaryTree tree, Void p) { + switch (tree.getKind()) { + case PREFIX_DECREMENT, POSTFIX_DECREMENT, PREFIX_INCREMENT, POSTFIX_INCREMENT -> { + if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getExpression()) + == indexVariableName) { + bodyIsIllegal = true; + return null; } - }; + } + default -> {} + } + return super.visitUnary(tree, p); + } - for (StatementTree stmt : statements) { - scanner.scan(stmt, null); + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + if (CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()) + == indexVariableName) { + bodyIsIllegal = true; + return null; + } + return super.visitCompoundAssignment(tree, p); } - if (!blockIsIllegal.get() && collectionElementTree[0] != null) { - return collectionElementTree[0]; + + @Override + public Void visitAssignment(AssignmentTree tree, Void p) { + Name assignedVariable = + CollectionOwnershipUtils.getNameFromExpressionTree(tree.getVariable()); + // Invalidate if writes to the collection or the loop index variable. + if (assignedVariable == indexVariableName || assignedVariable == collectionName) { + bodyIsIllegal = true; + return null; + } + return super.visitAssignment(tree, p); } - return null; - } - /** - * Returns true if the given tree is of the form collection.get(i), where i is the given index - * name. - * - * @param tree the tree to check - * @param index the index variable name - * @return true if the given tree is of the form collection.get(index) - */ - private boolean isIthCollectionElement(Tree tree, Name index) { - if (tree == null || index == null) { - return false; + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + if (isIthCollectionElement(tree, indexVariableName) + && collectionName == CollectionOwnershipUtils.getNameFromExpressionTree(tree)) { + // The last matching access represents the iterated element for this loop. + lastCollectionElementAccess = tree; + } + return super.visitMethodInvocation(tree, p); } - if (tree instanceof MethodInvocationTree mit - && index - == CollectionOwnershipUtils.getNameFromExpressionTree( - TreeUtils.getIdxForGetCall(tree))) { - ExpressionTree methodSelect = mit.getMethodSelect(); - if (methodSelect instanceof MemberSelectTree mst) { - return coAtf.isResourceCollection(mst.getExpression()); + + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { + // A lambda body is not executed as part of the enclosing loop body. + return null; + } + + @Override + public Void visitClass(ClassTree tree, Void p) { + // Skip local and anonymous class bodies for the same reason as lambdas. + return null; + } + + @Override + public @Nullable Void scan(@Nullable Tree tree, Void p) { + // Short-circuit the scanner if the collection/index variable is mutated. + if (bodyIsIllegal || tree == null) { + return null; } + return super.scan(tree, p); + } + + @Override + public @Nullable Void scan(@Nullable Iterable trees, Void p) { + if (bodyIsIllegal || trees == null) { + return null; + } + return super.scan(trees, p); } - return false; } } From edd7f0be4b90a6acc39381287cd4c5098cc75061 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Thu, 7 May 2026 21:37:11 -0700 Subject: [PATCH 371/374] review on while disposal loop matcher. --- .../WhileDisposalLoopMatcher.java | 1185 +++++++++-------- 1 file changed, 636 insertions(+), 549 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index 00f714b98abe..03c421ec92d3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -2,9 +2,10 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; @@ -15,15 +16,13 @@ import com.sun.source.tree.WhileLoopTree; import com.sun.source.util.TreeScanner; import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Collections; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; @@ -34,13 +33,34 @@ import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.TreeUtils; -/** Matches `while` {@link DisposalLoopInfo} that iterates over a resource collection. */ +/** + * Matches {@code while} loops that iterate over elements of a resource collection and produce + * {@link DisposalLoopInfo}. + * + *

Supported forms include iterator loops such as {@code while (it.hasNext())} and non-empty + * collection loops such as {@code while (!queue.isEmpty())}, {@code while (queue.size() > 0)}, and + * {@code while (0 < queue.size())}. A match requires exactly one extraction call in the loop body, + * such as {@code it.next()}, {@code queue.poll()}, or {@code queue.removeFirst()}. + */ final class WhileDisposalLoopMatcher { + /** + * Methods that may extract an element in an iterator-header loop like {@code while (it.hasNext()) + * { ... it.next() ... }}. + */ + private static final Set ITERATOR_EXTRACT_METHODS = Set.of("next"); + + /** + * Methods that may extract an element in a non-empty-collection loop such as {@code while + * (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ...}}, including {@code size() > 0} and + * {@code 0 < size()} variants. + */ + private static final Set NONEMPTY_EXTRACT_METHODS = + Set.of("poll", "pollFirst", "pollLast", "remove", "removeFirst", "removeLast", "pop"); + /** The CO type factory used for collection-ownership queries. */ private final CollectionOwnershipAnnotatedTypeFactory coAtf; @@ -54,7 +74,7 @@ final class WhileDisposalLoopMatcher { private @Nullable WhileLoopResolutionCache whileLoopCache; /** - * Creates a matcher for `while` disposal loops. + * Creates a matcher for {@code while} disposal loops. * * @param coAtf the CO type factory * @param rlccAtf the RLCC type factory @@ -69,486 +89,256 @@ final class WhileDisposalLoopMatcher { this.cfg = cfg; } - /** Lazily-computed CFG facts used to resolve potentially fulfilling while loops. */ - private static final class WhileLoopResolutionCache { - - /** A back edge in the CFG. */ - private static final class BlockEdge { - /** Source block of the back edge. */ - final Block sourceBlock; - - /** Target block of the back edge. */ - final Block targetBlock; - - /** - * Creates a CFG back edge description. - * - * @param sourceBlock source block of the back edge - * @param targetBlock target block of the back edge - */ - BlockEdge(Block sourceBlock, Block targetBlock) { - this.sourceBlock = sourceBlock; - this.targetBlock = targetBlock; - } - } - - /** Reachable CFG blocks in the current method. */ - private final Set reachableBlocks; - - /** Back edges among {@link #reachableBlocks}. */ - private final List backEdges; - - /** Natural loops for back edges, computed lazily. */ - private final IdentityHashMap> naturalLoopsByBackEdge = - new IdentityHashMap<>(); - - /** - * Creates CFG facts for resolving potentially fulfilling while loops in the given CFG. - * - * @param cfg the enclosing method CFG - */ - private WhileLoopResolutionCache(ControlFlowGraph cfg) { - Block entryBlock = cfg.getEntryBlock(); - this.reachableBlocks = CollectionOwnershipUtils.reachableFrom(entryBlock); - Map> dominators = computeDominators(entryBlock, reachableBlocks); - this.backEdges = findBackEdges(reachableBlocks, dominators); - } - - /** - * Returns the back edges among the reachable blocks in the current CFG. - * - * @return the CFG back edges - */ - private List getBackEdges() { - return backEdges; - } - - /** - * Returns the natural loop induced by the given back edge, computing it lazily if needed. - * - * @param backEdge the back edge - * @return the natural loop induced by the given back edge - */ - private Set getNaturalLoopForBackEdge(BlockEdge backEdge) { - return naturalLoopsByBackEdge.computeIfAbsent( - backEdge, - ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); - } - - /** - * Computes dominators for the reachable blocks in the current CFG. - * - * @param entryBlock the CFG entry block - * @param reachableBlocks reachable blocks in the CFG - * @return dominators for each reachable block - */ - private static Map> computeDominators( - Block entryBlock, Set reachableBlocks) { - Map> dominators = new HashMap<>(); - - for (Block block : reachableBlocks) { - if (block.equals(entryBlock)) { - dominators.put(block, new HashSet<>(Collections.singleton(entryBlock))); - } else { - dominators.put(block, new HashSet<>(reachableBlocks)); - } - } - - boolean changed; - do { - changed = false; - for (Block block : reachableBlocks) { - if (block.equals(entryBlock)) { - continue; - } - - Set newDominators = null; - for (Block predecessor : block.getPredecessors()) { - if (predecessor == null || !reachableBlocks.contains(predecessor)) { - continue; - } - Set predecessorDominators = dominators.get(predecessor); - if (predecessorDominators == null) { - continue; - } - if (newDominators == null) { - newDominators = new HashSet<>(predecessorDominators); - } else { - newDominators.retainAll(predecessorDominators); - } - } - - if (newDominators == null) { - newDominators = new HashSet<>(); - } - newDominators.add(block); - - if (!newDominators.equals(dominators.get(block))) { - dominators.put(block, newDominators); - changed = true; - } - } - } while (changed); - - return dominators; - } - - /** - * Returns the back edges among the reachable blocks in the current CFG. - * - * @param reachableBlocks reachable blocks in the CFG - * @param dominators dominators for each reachable block - * @return the CFG back edges - */ - private static List findBackEdges( - Set reachableBlocks, Map> dominators) { - java.util.List backEdges = new java.util.ArrayList<>(); - for (Block sourceBlock : reachableBlocks) { - for (Block targetBlock : sourceBlock.getSuccessors()) { - if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { - continue; - } - Set sourceDominators = dominators.get(sourceBlock); - if (sourceDominators != null && sourceDominators.contains(targetBlock)) { - backEdges.add(new BlockEdge(sourceBlock, targetBlock)); - } - } - } - return backEdges; - } - - /** - * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. - * - * @param sourceBlock the source of the back edge - * @param targetBlock the target of the back edge - * @param reachableBlocks reachable blocks in the CFG - * @return the natural loop induced by the back edge - */ - private static Set naturalLoop( - Block sourceBlock, Block targetBlock, Set reachableBlocks) { - Set loopBlocks = new HashSet<>(); - ArrayDeque stack = new ArrayDeque<>(); - - loopBlocks.add(targetBlock); - if (loopBlocks.add(sourceBlock)) { - stack.push(sourceBlock); - } - - while (!stack.isEmpty()) { - Block block = stack.pop(); - for (Block predecessor : block.getPredecessors()) { - if (predecessor == null || !reachableBlocks.contains(predecessor)) { - continue; - } - if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { - stack.push(predecessor); - } - } - } - return loopBlocks; - } - } - /** - * Description of a supported while disposal loop header form. + * Returns the {@link DisposalLoopInfo} if the {@code while} loop {@code tree} matches one of the + * supported header forms and has exactly one compatible extraction in its body. * - *

Each header form determines which extraction methods are allowed in the loop body. - */ - private static final class WhileSpec { - /** Methods that may extract an element when this header form is used. */ - final Set extractMethods; - - /** - * Creates a while-loop header specification. - * - * @param extractMethods methods that may extract an element from the looped collection - */ - WhileSpec(Set extractMethods) { - this.extractMethods = extractMethods; - } - } - - /** Iterator form: {@code while (it.hasNext()) { ... it.next() ... }}. */ - private static final WhileSpec ITERATOR_SPEC = new WhileSpec(Collections.singleton("next")); - - /** - * Non-empty collection form: {@code while (!c.isEmpty()) { ... c.poll()/pop/removeFirst/... ... - * }}, including {@code size() > 0} and {@code 0 < size()} variants. - */ - private static final WhileSpec NONEMPTY_SPEC = - new WhileSpec( - new HashSet<>( - Arrays.asList( - "poll", "pollFirst", "pollLast", "remove", "removeFirst", "removeLast", "pop"))); - - /** - * AST facts recovered from a matched while-loop header. + *

For example: * - *

{@link #collectionTree} is the collection whose element obligations may be discharged. - * {@link #headerVar} is the iterator or collection variable constrained by the header. {@link - * #collectionVarNameForBailout} names the collection variable whose writes should invalidate the - * match when present. - */ - private static final class WhileHeaderMatch { - /** Collection expression whose element obligations may be discharged. */ - final ExpressionTree collectionTree; - - /** Collection variable name whose writes should invalidate the match, if one exists. */ - final @Nullable Name collectionVarNameForBailout; - - /** Iterator variable or collection variable constrained by the loop header. */ - final Name headerVar; - - /** Accepted extraction shape for the matched loop header. */ - final WhileSpec spec; - - /** - * Creates a summary of the AST facts recovered from a matched while-loop header. - * - * @param collectionTree the owning collection expression to mark - * @param collectionVarNameForBailout collection variable whose writes invalidate the match - * @param headerVar iterator or collection variable constrained by the header - * @param spec accepted extraction shape for the matched loop header - */ - WhileHeaderMatch( - ExpressionTree collectionTree, - @Nullable Name collectionVarNameForBailout, - Name headerVar, - WhileSpec spec) { - this.collectionTree = collectionTree; - this.collectionVarNameForBailout = collectionVarNameForBailout; - this.headerVar = headerVar; - this.spec = spec; - } - } - - /** - * One extracted element use recovered from a while-loop body. + *

{@code
+   * while (it.hasNext()) {
+   *   Resource resource = it.next();
+   *   resource.close();
+   * }
+   * }
* - *

The extraction call is the expression that removes or advances to the next element, such as - * {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. - */ - private static final class BodyExtraction { - /** Extraction call such as {@code it.next()}, {@code q.poll()}, or {@code s.pop()}. */ - final MethodInvocationTree extractionCall; - - /** - * Creates a body extraction summary. - * - * @param extractionCall extraction call found in the loop body - */ - BodyExtraction(MethodInvocationTree extractionCall) { - this.extractionCall = extractionCall; - } - } - - /** - * Matches a {@link DisposalLoopInfo} that uses a while-loop and resolves its CFG-local loop - * facts. + * and: * - *

Supported header shapes are iterator loops such as {@code while (it.hasNext())} and - * non-empty collection loops such as {@code while (!q.isEmpty())}, {@code while (q.size() > 0)}, - * and {@code while (0 < q.size())}. + *

{@code
+   * while (!queue.isEmpty()) {
+   *   queue.removeFirst().close();
+   * }
+   * }
* * @param tree the while-loop to inspect - * @return the matched disposal loop, or {@code null} if the loop does not match + * @return the matched disposal loop info, or {@code null} if the loop does not match */ @Nullable DisposalLoopInfo match(WhileLoopTree tree) { - ExpressionTree condNoParens = TreeUtils.withoutParens(tree.getCondition()); - WhileHeaderMatch header = matchWhileHeader(condNoParens); - if (header == null) { - return null; - } - StatementTree loopBodyStatement = tree.getStatement(); - List bodyStatements = - loopBodyStatement instanceof BlockTree blockTree - ? blockTree.getStatements() - : List.of(loopBodyStatement); - if (bodyStatements == null) { + ExpressionTree conditionWithoutParens = TreeUtils.withoutParens(tree.getCondition()); + WhileHeaderInfo headerMatch = matchWhileHeader(conditionWithoutParens); + if (headerMatch == null) { return null; } - BodyExtraction extraction = - findSingleExtractionInWhileBody( - bodyStatements, - header.headerVar, - header.collectionVarNameForBailout, - header.spec.extractMethods); - if (extraction == null) { + + // Validate the loop body and recover the unique extraction call that represents the iterated + // element for this loop. + MethodInvocationTree extractionCall = + new WhileBodyScanner( + headerMatch.headerVarName, + headerMatch.collectionVarNameForInvalidation, + headerMatch.allowedExtractMethods) + .scanLoopBody(tree.getStatement()); + if (extractionCall == null) { return null; } - Block condBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, condNoParens); - if (condBlock == null) { + + // After the tree match succeeds, recover the CFG blocks corresponding to the loop condition, + // body entry, and loop back edge. + Block conditionBlock = CollectionOwnershipUtils.firstBlockForTree(cfg, conditionWithoutParens); + if (conditionBlock == null) { return null; } - ConditionalBlock cblock = findConditionalSuccessor(condBlock); - if (cblock == null) { - Block peeled = peelExceptionBlocksToPred(condBlock); - if (peeled != null) { - cblock = findConditionalSuccessor(peeled); + + ConditionalBlock conditionalBlock = findImmediateConditionalSuccessor(conditionBlock); + if (conditionalBlock == null) { + // Some while-loop conditions are represented through exception blocks before reaching the + // actual conditional branch. Retry from the peeled predecessor. + Block peeledPredecessor = peelExceptionBlocksToPredecessor(conditionBlock); + if (peeledPredecessor != null) { + conditionalBlock = findImmediateConditionalSuccessor(peeledPredecessor); } } - if (cblock == null) { + if (conditionalBlock == null) { return null; } - Block loopBodyEntryBlock = cblock.getThenSuccessor(); - Node elementNode = CollectionOwnershipUtils.anyNodeForTree(cfg, extraction.extractionCall); - if (elementNode == null) { + Block loopBodyEntryBlock = conditionalBlock.getThenSuccessor(); + Node iteratedElementNode = CollectionOwnershipUtils.anyNodeForTree(cfg, extractionCall); + if (iteratedElementNode == null) { return null; } Block loopUpdateBlock = - chooseLoopUpdateBlockForPotentiallyFulfillingLoop( - loopBodyEntryBlock, cblock, getOrCreateWhileLoopCache()); - if (loopUpdateBlock != null) { - return new DisposalLoopInfo( - header.collectionTree, - extraction.extractionCall, - elementNode, - CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, condNoParens), - cblock, - loopBodyEntryBlock, - loopUpdateBlock); + chooseLoopUpdateBlock(loopBodyEntryBlock, conditionalBlock, getOrCreateWhileLoopCache()); + if (loopUpdateBlock == null) { + return null; } - return null; + + return new DisposalLoopInfo( + headerMatch.collectionTree, + extractionCall, + iteratedElementNode, + CollectionOwnershipUtils.cfgAssociatedTreeFor(cfg, conditionWithoutParens), + conditionalBlock, + loopBodyEntryBlock, + loopUpdateBlock); } /** - * Matches supported while-loop header forms and returns the recovered loop facts. + * Matches supported {@code while} header forms and returns the recovered {@link WhileHeaderInfo}. + * + *

Supported header shapes are: * - *

Supported forms are: {@code while (it.hasNext())}, {@code while (!c.isEmpty())}, {@code - * while (c.size() > 0)}, and {@code while (0 < c.size())}. + *

    + *
  • {@code it.hasNext()} + *
  • {@code !collection.isEmpty()} + *
  • {@code collection.size() > 0} + *
  • {@code 0 < collection.size()} + *
* - * @param cond the while-loop condition with parentheses removed + * @param condition the while-loop condition with parentheses removed * @return the recovered header facts, or {@code null} if the header is unsupported */ - private @Nullable WhileHeaderMatch matchWhileHeader(ExpressionTree cond) { - if (cond instanceof MethodInvocationTree mit) { - if (TreeUtils.isHasNextCall(mit)) { - ExpressionTree recv = receiverOfInvocation(mit); - Name itName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); - if (itName == null) { - return null; - } - ExpressionTree colExpr = recoverCollectionFromIteratorReceiver(recv); - if (colExpr == null) { - return null; - } - Name colName = CollectionOwnershipUtils.getNameFromExpressionTree(colExpr); - return new WhileHeaderMatch(colExpr, colName, itName, ITERATOR_SPEC); - } + private @Nullable WhileHeaderInfo matchWhileHeader(ExpressionTree condition) { + if (condition instanceof MethodInvocationTree methodInvocationTree) { + return matchIteratorHeaderInfo(methodInvocationTree); } - if (cond instanceof UnaryTree unaryTreeCond && cond.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { - ExpressionTree inner = TreeUtils.withoutParens(unaryTreeCond.getExpression()); - WhileHeaderMatch m = matchNonEmptyFromExpr(inner); - if (m != null) { - return m; + if (condition instanceof UnaryTree unaryTree + && condition.getKind() == Tree.Kind.LOGICAL_COMPLEMENT) { + ExpressionTree inner = TreeUtils.withoutParens(unaryTree.getExpression()); + WhileHeaderInfo headerMatch = matchNonEmptyFromExpr(inner); + if (headerMatch != null) { + return headerMatch; } } - if (cond instanceof BinaryTree binaryTreeCond) { - WhileHeaderMatch m = matchNonEmptyFromSize(binaryTreeCond); - if (m != null) { - return m; - } + if (condition instanceof BinaryTree binaryTree) { + return matchNonEmptyFromSize(binaryTree); } return null; } /** - * Matches a non-empty collection condition of the form {@code !c.isEmpty()}. + * Matches an iterator header of the form {@code while (it.hasNext())} and extracts corresponding + * {@link WhileHeaderInfo}. * - * @param inner the expression under the logical complement - * @return the recovered header facts, or {@code null} if the expression does not match + * @param invocation the candidate header invocation + * @return the recovered header facts, or {@code null} if the header does not match */ - private @Nullable WhileHeaderMatch matchNonEmptyFromExpr(ExpressionTree inner) { - if (!(inner instanceof MethodInvocationTree mit)) { + private @Nullable WhileHeaderInfo matchIteratorHeaderInfo(MethodInvocationTree invocation) { + if (!TreeUtils.isHasNextCall(invocation)) { return null; } - if (!isIsEmptyCall(mit)) { + ExpressionTree receiver = receiverOfInvocation(invocation); + if (receiver == null) { return null; } - ExpressionTree recv = receiverOfInvocation(mit); - if (recv == null) { + + Name iteratorVarName = CollectionOwnershipUtils.getNameFromExpressionTree(receiver); + if (iteratorVarName == null) { return null; } - Name varName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); - if (varName == null) { + + ExpressionTree collectionTree = recoverCollectionFromIteratorReceiver(receiver); + if (collectionTree == null) { return null; } - if (!coAtf.isResourceCollection(recv)) { + + Name collectionVarName = CollectionOwnershipUtils.getNameFromExpressionTree(collectionTree); + return new WhileHeaderInfo( + collectionTree, collectionVarName, iteratorVarName, ITERATOR_EXTRACT_METHODS); + } + + /** + * Matches a non-empty {@code while} condition of the form {@code !collection.isEmpty()} and + * extracts corresponding {@link WhileHeaderInfo}. + * + * @param expression the expression under the logical complement + * @return the recovered header facts, or {@code null} if the expression does not match + */ + private @Nullable WhileHeaderInfo matchNonEmptyFromExpr(ExpressionTree expression) { + if (!(expression instanceof MethodInvocationTree methodInvocationTree)) { return null; } - ExpressionTree colTree = CollectionOwnershipUtils.baseExpression(recv); - if (colTree == null) { + if (!isIsEmptyCall(methodInvocationTree)) { return null; } - return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + + ExpressionTree receiver = receiverOfInvocation(methodInvocationTree); + if (receiver == null) { + return null; + } + return nonEmptyHeaderMatch(receiver); } /** - * Matches a non-empty collection condition of the form {@code c.size() > 0} or {@code 0 < - * c.size()}. + * Matches a non-empty collection condition of the form {@code collection.size() > 0} or {@code 0 + * < collection.size()} and returns corresponding {@link WhileHeaderInfo}. * * @param condition the binary condition * @return the recovered header facts, or {@code null} if the expression does not match */ - private @Nullable WhileHeaderMatch matchNonEmptyFromSize(BinaryTree condition) { - Tree.Kind k = condition.getKind(); - if (k != Tree.Kind.GREATER_THAN && k != Tree.Kind.LESS_THAN) { + private @Nullable WhileHeaderInfo matchNonEmptyFromSize(BinaryTree condition) { + Tree.Kind kind = condition.getKind(); + if (kind != Tree.Kind.GREATER_THAN && kind != Tree.Kind.LESS_THAN) { return null; } ExpressionTree left = TreeUtils.withoutParens(condition.getLeftOperand()); ExpressionTree right = TreeUtils.withoutParens(condition.getRightOperand()); - MethodInvocationTree sizeCall = null; - LiteralTree zero = null; + @Nullable MethodInvocationTree sizeCall = null; + @Nullable LiteralTree zeroLiteral = null; - if (k == Tree.Kind.GREATER_THAN) { - if (left instanceof MethodInvocationTree mitLeft && right instanceof LiteralTree ltRight) { - sizeCall = mitLeft; - zero = ltRight; + if (kind == Tree.Kind.GREATER_THAN) { + if (left instanceof MethodInvocationTree leftInvocation + && right instanceof LiteralTree rightLiteral) { + sizeCall = leftInvocation; + zeroLiteral = rightLiteral; } } else { - if (left instanceof LiteralTree ltLeft && right instanceof MethodInvocationTree ltRight) { - zero = ltLeft; - sizeCall = ltRight; + if (left instanceof LiteralTree leftLiteral + && right instanceof MethodInvocationTree rightInvocation) { + sizeCall = rightInvocation; + zeroLiteral = leftLiteral; } } - if (sizeCall == null - || !(zero.getValue() instanceof Integer) - || (Integer) zero.getValue() != 0) { + if (sizeCall == null) { + return null; + } + + Object zeroValue = zeroLiteral.getValue(); + if (!(zeroValue instanceof Integer intZeroVal) || intZeroVal != 0) { return null; } if (!TreeUtils.isSizeAccess(sizeCall)) { return null; } - ExpressionTree recv = receiverOfInvocation(sizeCall); - if (recv == null) { + ExpressionTree receiver = receiverOfInvocation(sizeCall); + if (receiver == null) { return null; } + return nonEmptyHeaderMatch(receiver); + } - Name varName = CollectionOwnershipUtils.getNameFromExpressionTree(recv); - if (varName == null) { + /** + * Builds a {@link WhileHeaderInfo} for a non-empty collection header once the receiver expression + * has already been recovered. + * + * @param receiver the receiver checked by the non-empty header + * @return the recovered header facts, or {@code null} if the receiver is not a resource + * collection + */ + private @Nullable WhileHeaderInfo nonEmptyHeaderMatch(ExpressionTree receiver) { + Name collectionVarName = CollectionOwnershipUtils.getNameFromExpressionTree(receiver); + if (collectionVarName == null) { return null; } - - if (!coAtf.isResourceCollection(recv)) { + if (!coAtf.isResourceCollection(receiver)) { return null; } - ExpressionTree colTree = CollectionOwnershipUtils.baseExpression(recv); - if (colTree == null) { + ExpressionTree collectionTree = CollectionOwnershipUtils.baseExpression(receiver); + if (collectionTree == null) { return null; } - return new WhileHeaderMatch(colTree, varName, varName, NONEMPTY_SPEC); + return new WhileHeaderInfo( + collectionTree, collectionVarName, collectionVarName, NONEMPTY_EXTRACT_METHODS); } /** @@ -558,11 +348,12 @@ private static final class BodyExtraction { * @return true if {@code invocation} is an {@code isEmpty()} call with no arguments */ private boolean isIsEmptyCall(MethodInvocationTree invocation) { - ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree ms)) { + ExpressionTree methodSelect = invocation.getMethodSelect(); + if (!(methodSelect instanceof MemberSelectTree memberSelectTree)) { return false; } - return ms.getIdentifier().contentEquals("isEmpty") && invocation.getArguments().isEmpty(); + return memberSelectTree.getIdentifier().contentEquals("isEmpty") + && invocation.getArguments().isEmpty(); } /** @@ -572,8 +363,8 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { * @return the explicit receiver, or {@code null} if none exists */ private @Nullable ExpressionTree receiverOfInvocation(MethodInvocationTree invocation) { - ExpressionTree sel = invocation.getMethodSelect(); - if (sel instanceof MemberSelectTree memberSelectTree) { + ExpressionTree methodSelect = invocation.getMethodSelect(); + if (methodSelect instanceof MemberSelectTree memberSelectTree) { return memberSelectTree.getExpression(); } return null; @@ -583,242 +374,331 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { * Recovers the collection expression from an iterator receiver in a header such as {@code while * (it.hasNext())}. * - *

This only recognizes local iterator variables initialized by {@code col.iterator()}. + *

This only recognizes local iterator variables initialized directly by {@code + * collection.iterator()}. * - * @param iteratorExpr the iterator receiver expression + * @param iteratorReceiver the iterator receiver expression * @return the collection expression, or {@code null} if it cannot be recovered */ private @Nullable ExpressionTree recoverCollectionFromIteratorReceiver( - ExpressionTree iteratorExpr) { - if (iteratorExpr == null) { + ExpressionTree iteratorReceiver) { + Element iteratorElement = TreeUtils.elementFromTree(iteratorReceiver); + if (!(iteratorElement instanceof VariableElement)) { return null; } - - Element itElt = TreeUtils.elementFromTree(iteratorExpr); - if (!(itElt instanceof VariableElement)) { + if (iteratorElement.getKind() != ElementKind.LOCAL_VARIABLE) { return null; } - if (itElt.getKind() != ElementKind.LOCAL_VARIABLE) { + Tree declaration = rlccAtf.declarationFromElement(iteratorElement); + if (!(declaration instanceof VariableTree variableTreeDeclaration)) { return null; } - Tree decl = rlccAtf.declarationFromElement(itElt); - if (!(decl instanceof VariableTree variableTreeDecl)) { + ExpressionTree initializer = variableTreeDeclaration.getInitializer(); + if (!(initializer instanceof MethodInvocationTree initializerCall)) { return null; } - ExpressionTree init = variableTreeDecl.getInitializer(); - if (!(init instanceof MethodInvocationTree initCall)) { + ExpressionTree methodSelect = initializerCall.getMethodSelect(); + if (!(methodSelect instanceof MemberSelectTree memberSelectTree)) { return null; } - - ExpressionTree sel = initCall.getMethodSelect(); - if (!(sel instanceof MemberSelectTree ms)) { + if (!memberSelectTree.getIdentifier().contentEquals("iterator") + || !initializerCall.getArguments().isEmpty()) { return null; } - if (!ms.getIdentifier().contentEquals("iterator") || !initCall.getArguments().isEmpty()) { + ExpressionTree collectionExpression = memberSelectTree.getExpression(); + if (!coAtf.isResourceCollection(collectionExpression)) { return null; } + return CollectionOwnershipUtils.baseExpression(collectionExpression); + } - ExpressionTree colExpr = ms.getExpression(); - if (!coAtf.isResourceCollection(colExpr)) { - return null; + /** + * Returns whether the given invocation is an allowed extraction call on the matched header + * variable. + * + * @param invocation a method invocation + * @param headerVarName the iterator or collection variable constrained by the header + * @param allowedExtractMethods extraction methods permitted by the matched header form + * @return true if {@code invocation} is an allowed extraction call on {@code headerVarName} + */ + private boolean isExtractionCallOnHeaderVar( + MethodInvocationTree invocation, Name headerVarName, Set allowedExtractMethods) { + ExpressionTree methodSelect = invocation.getMethodSelect(); + if (!(methodSelect instanceof MemberSelectTree memberSelectTree)) { + return false; } - return CollectionOwnershipUtils.baseExpression(colExpr); + String methodName = memberSelectTree.getIdentifier().toString(); + if (!allowedExtractMethods.contains(methodName)) { + return false; + } + if (!invocation.getArguments().isEmpty()) { + return false; + } + + Name receiverName = + CollectionOwnershipUtils.getNameFromExpressionTree(memberSelectTree.getExpression()); + return receiverName != null && receiverName == headerVarName; } /** - * Finds exactly one extraction in the loop body. If 0 or >1 extractions occur, returns {@code - * null}. + * Scans the body of a matched {@code while} loop and checks whether the body is consistent with + * the loop header. * - *

This matcher rejects writes to the iterator/header variable and, when present, to the - * collection variable itself, because such writes invalidate the header/body correspondence used - * by later CFG verification. + *

The body is accepted only if it: * - * @param statements the loop body statements - * @param headerVar the iterator or collection variable constrained by the header - * @param collectionVarName the collection variable to protect from writes, if any - * @param allowedExtractMethods the extraction methods allowed by the matched header - * @return the unique extraction in the loop body, or {@code null} if the body is unsupported + *

    + *
  • does not overwrite the header variable or the matched collection variable + *
  • contains exactly one allowed extraction call on the header/collection variable + *
*/ - private @Nullable BodyExtraction findSingleExtractionInWhileBody( - List statements, - Name headerVar, - @Nullable Name collectionVarName, - Set allowedExtractMethods) { + private final class WhileBodyScanner extends TreeScanner { - AtomicBoolean illegal = new AtomicBoolean(false); - final MethodInvocationTree[] extraction = new MethodInvocationTree[] {null}; - final int[] extractionCount = new int[] {0}; + /** Iterator or collection variable constrained by the loop header. */ + private final Name headerVarName; - TreeScanner scanner = - new TreeScanner() { + /** Collection variable whose writes should invalidate the match, if one exists. */ + private final @Nullable Name collectionVarNameForInvalidation; - private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { - Name assigned = CollectionOwnershipUtils.getNameFromExpressionTree(lhs); - if (assigned != null) { - if (assigned == headerVar) illegal.set(true); - if (collectionVarName != null && assigned == collectionVarName) illegal.set(true); - } - } + /** Extraction methods allowed by the matched header form. */ + private final Set allowedExtractMethods; - private void recordExtractionIfAny(ExpressionTree expr) { - expr = TreeUtils.withoutParens(expr); - if (!(expr instanceof MethodInvocationTree mit)) { - return; - } + /** + * Whether the loop body has been rejected. The loop is rejected if it overwrites the + * header/collection variable, or does zero/more than one extraction call. + */ + private boolean illegal = false; - if (!isExtractionCallOnHeaderVar(mit, headerVar, allowedExtractMethods)) { - return; - } + /** Number of extraction calls found so far. */ + private int extractionCount = 0; - extractionCount[0]++; - if (extractionCount[0] > 1) { - illegal.set(true); - return; - } - extraction[0] = mit; - } + /** The unique extraction call, if one has been found so far. */ + private @Nullable MethodInvocationTree extractionCall = null; - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - markWriteIfTargetsHeaderOrCollection(node.getVariable()); - return super.visitCompoundAssignment(node, p); - } + /** + * Creates a {@link WhileBodyScanner}. + * + * @param headerVarName iterator or collection variable constrained by the header + * @param collectionVarNameForInvalidation collection variable whose writes invalidate the match + * @param allowedExtractMethods extraction methods allowed by the matched header form + */ + private WhileBodyScanner( + Name headerVarName, + @Nullable Name collectionVarNameForInvalidation, + Set allowedExtractMethods) { + this.headerVarName = headerVarName; + this.collectionVarNameForInvalidation = collectionVarNameForInvalidation; + this.allowedExtractMethods = allowedExtractMethods; + } - @Override - public Void visitAssignment(AssignmentTree node, Void p) { - markWriteIfTargetsHeaderOrCollection(node.getVariable()); - recordExtractionIfAny(node.getExpression()); - return super.visitAssignment(node, p); - } + /** + * Scans the loop body once and returns the unique extraction call as the iterated element. + * + * @param loopBody the loop body to scan + * @return the unique allowed extraction call, or {@code null} if the body writes to the header + * variable or matched collection variable, contains no extraction, or contains more than + * one extraction + */ + private @Nullable MethodInvocationTree scanLoopBody(StatementTree loopBody) { + super.scan(loopBody, null); + if (illegal || extractionCount != 1) { + return null; + } + return extractionCall; + } - @Override - public Void visitVariable(VariableTree vt, Void p) { - ExpressionTree init = vt.getInitializer(); - if (init != null) { - recordExtractionIfAny(init); - } - return super.visitVariable(vt, p); - } + /** + * Rejects a loop body that writes to the variable constrained by the header/collection + * variable. + * + * @param lhs the assignment target + */ + private void markWriteIfTargetsHeaderOrCollection(ExpressionTree lhs) { + Name assignedVariable = CollectionOwnershipUtils.getNameFromExpressionTree(lhs); + if (assignedVariable == null) { + return; + } + if (assignedVariable == headerVarName) { + illegal = true; + } + if (collectionVarNameForInvalidation != null + && assignedVariable == collectionVarNameForInvalidation) { + illegal = true; + } + } - @Override - public Void visitMethodInvocation(MethodInvocationTree mit, Void p) { - ExpressionTree sel = mit.getMethodSelect(); - if (sel instanceof MemberSelectTree memberSelect) { - ExpressionTree recv = memberSelect.getExpression(); - recordExtractionIfAny(recv); - } - return super.visitMethodInvocation(mit, p); - } - }; + /** + * Records one allowed extraction call found in the given expression. + * + * @param expression the candidate extraction expression + */ + private void recordExtractionIfAny(ExpressionTree expression) { + ExpressionTree expressionWithoutParens = TreeUtils.withoutParens(expression); + if (!(expressionWithoutParens instanceof MethodInvocationTree methodInvocationTree)) { + return; + } + if (!isExtractionCallOnHeaderVar( + methodInvocationTree, headerVarName, allowedExtractMethods)) { + return; + } - for (StatementTree st : statements) { - scanner.scan(st, null); - if (illegal.get()) break; + extractionCount++; + if (extractionCount > 1) { + // More than one extraction means this iteration can advance through more than one element, + // so the loop no longer corresponds to a single iterated element. + illegal = true; + return; + } + extractionCall = methodInvocationTree; + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + markWriteIfTargetsHeaderOrCollection(tree.getVariable()); + if (illegal) { + return null; + } + return super.visitCompoundAssignment(tree, p); } - if (illegal.get() || extraction[0] == null || extractionCount[0] != 1) { + @Override + public Void visitAssignment(AssignmentTree tree, Void p) { + // Reassigning the header/collection variable breaks the link between the loop header and the + // extraction expected in the body. + markWriteIfTargetsHeaderOrCollection(tree.getVariable()); + if (illegal) { + return null; + } + recordExtractionIfAny(tree.getExpression()); + return super.visitAssignment(tree, p); + } + + @Override + public Void visitVariable(VariableTree tree, Void p) { + ExpressionTree initializer = tree.getInitializer(); + if (initializer != null) { + recordExtractionIfAny(initializer); + } + return super.visitVariable(tree, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + ExpressionTree receiver = receiverOfInvocation(tree); + if (receiver != null) { + // For chained calls such as `it.next().close()` or `queue.poll().close()`, the extraction + // call appears as the receiver of an outer invocation. + recordExtractionIfAny(receiver); + } + return super.visitMethodInvocation(tree, p); + } + + @Override + public Void visitUnary(UnaryTree tree, Void p) { + switch (tree.getKind()) { + case PREFIX_DECREMENT, POSTFIX_DECREMENT, PREFIX_INCREMENT, POSTFIX_INCREMENT -> { + Name mutatedVariable = + CollectionOwnershipUtils.getNameFromExpressionTree(tree.getExpression()); + if (mutatedVariable == headerVarName) { + illegal = true; + return null; + } + } + default -> {} + } + return super.visitUnary(tree, p); + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { + // A lambda body is not executed as part of the enclosing loop body. return null; } - return new BodyExtraction(extraction[0]); - } - /** - * Returns whether the given invocation is an allowed extraction call on the matched header - * variable. - * - * @param invocation a method invocation - * @param headerVar the iterator or collection variable constrained by the header - * @param allowedExtractMethods extraction methods permitted by the matched header form - * @return true if {@code invocation} is an allowed extraction call on {@code headerVar} - */ - private boolean isExtractionCallOnHeaderVar( - MethodInvocationTree invocation, Name headerVar, Set allowedExtractMethods) { - ExpressionTree sel = invocation.getMethodSelect(); - if (!(sel instanceof MemberSelectTree ms)) { - return false; + @Override + public Void visitClass(ClassTree tree, Void p) { + // Skip local and anonymous class bodies for the same reason as lambdas. + return null; } - String methodName = ms.getIdentifier().toString(); - if (!allowedExtractMethods.contains(methodName)) { - return false; + + @Override + public @Nullable Void scan(@Nullable Tree tree, Void p) { + // Short-circuit if the body has been rejected. + if (illegal || tree == null) { + return null; + } + return super.scan(tree, p); } - if (!invocation.getArguments().isEmpty()) { - return false; + + @Override + public @Nullable Void scan(@Nullable Iterable trees, Void p) { + if (illegal || trees == null) { + return null; + } + return super.scan(trees, p); } - Name recv = CollectionOwnershipUtils.getNameFromExpressionTree(ms.getExpression()); - return recv != null && recv == headerVar; } /** - * Returns the conditional successor reached from the given block, if one is immediately visible. + * Returns the conditional successor reached immediately from the given block, if one is visible. * * @param block a CFG block - * @return the conditional successor of {@code block}, or {@code null} if none is found + * @return the immediate conditional successor of {@code block}, or {@code null} if none is found */ - private @Nullable ConditionalBlock findConditionalSuccessor(Block block) { - for (Block succ : block.getSuccessors()) { - if (succ instanceof ConditionalBlock conditionalBlockSucc) { - return conditionalBlockSucc; - } - } - if (block instanceof SingleSuccessorBlock singleSuccessorBlock) { - Block succ = singleSuccessorBlock.getSuccessor(); - if (succ instanceof ConditionalBlock conditionalBlockSucc) { - return conditionalBlockSucc; + private @Nullable ConditionalBlock findImmediateConditionalSuccessor(Block block) { + for (Block successor : block.getSuccessors()) { + if (successor instanceof ConditionalBlock conditionalBlock) { + return conditionalBlock; } } return null; } /** - * Walks backward through exception blocks to recover the predecessor block that leads to the - * actual loop conditional. - * - *

This is needed because loop conditions such as {@code iterator.hasNext()} may be represented - * by exception blocks before reaching the conditional branch. + * Walks backward through exception blocks amd returns the predecessor block that is not an {@link + * ExceptionBlock}. * * @param block a CFG block * @return a predecessor block to retry from, or {@code null} if no such block is found */ - private @Nullable Block peelExceptionBlocksToPred(Block block) { - Block cur = block; + private @Nullable Block peelExceptionBlocksToPredecessor(Block block) { + Block currentBlock = block; Set visitedBlocks = new HashSet<>(); - while (cur instanceof ExceptionBlock && visitedBlocks.add(cur)) { - Set preds = cur.getPredecessors(); - if (preds.size() != 1) { + while (currentBlock instanceof ExceptionBlock && visitedBlocks.add(currentBlock)) { + Set predecessors = currentBlock.getPredecessors(); + if (predecessors.size() != 1) { break; } - Block p = preds.iterator().next(); - if (p == null) { + Block predecessor = predecessors.iterator().next(); + if (predecessor == null) { break; } - cur = p; + currentBlock = predecessor; } - return cur; + return currentBlock; } /** - * Chooses the best loop update block for a potentially fulfilling while loop by matching it to - * the tightest natural loop that contains both the body entry and the loop condition. + * Chooses the loop-update block for a matched while loop by selecting the smallest natural loop + * that contains both the body entry and the loop conditional. * * @param bodyEntryBlock the loop body entry block * @param conditionalBlock the loop conditional block * @param whileLoopCache cached CFG facts for while-loop resolution - * @return the chosen loop update block, or {@code null} if none is found + * @return the chosen loop-update block, or {@code null} if none is found */ - private @Nullable Block chooseLoopUpdateBlockForPotentiallyFulfillingLoop( + private @Nullable Block chooseLoopUpdateBlock( Block bodyEntryBlock, ConditionalBlock conditionalBlock, WhileLoopResolutionCache whileLoopCache) { - Block bestLoopUpdateBlock = null; int bestLoopSize = Integer.MAX_VALUE; - for (WhileLoopResolutionCache.BlockEdge backEdge : whileLoopCache.getBackEdges()) { + for (WhileLoopResolutionCache.BackEdge backEdge : whileLoopCache.getBackEdges()) { Set naturalLoop = whileLoopCache.getNaturalLoopForBackEdge(backEdge); if (!naturalLoop.contains(bodyEntryBlock)) { @@ -828,6 +708,8 @@ private boolean isExtractionCallOnHeaderVar( continue; } + // Prefer the smallest natural loop containing both the body entry and the conditional block, + // which most closely matches the while loop being resolved. if (naturalLoop.size() < bestLoopSize) { bestLoopSize = naturalLoop.size(); bestLoopUpdateBlock = backEdge.targetBlock; @@ -848,4 +730,209 @@ private WhileLoopResolutionCache getOrCreateWhileLoopCache() { } return whileLoopCache; } + + /** + * Facts recovered from a matched {@code while} loop header. + * + * @param collectionTree collection expression whose element obligations may be discharged. + * @param collectionVarNameForInvalidation collection variable whose writes should invalidate the + * match, if one exists. + * @param headerVarName iterator variable or collection variable constrained by the loop header. + * @param allowedExtractMethods extraction methods accepted for this matched header. + */ + private record WhileHeaderInfo( + ExpressionTree collectionTree, + @Nullable Name collectionVarNameForInvalidation, + Name headerVarName, + Set allowedExtractMethods) { + + /** + * Creates a summary of the facts recovered from a matched while-loop header. + * + * @param collectionTree the owning collection expression to mark + * @param collectionVarNameForInvalidation collection variable whose writes invalidate the match + * @param headerVarName iterator or collection variable constrained by the header + * @param allowedExtractMethods extraction methods accepted for this matched header + */ + private WhileHeaderInfo {} + } + + /** Lazily-computed CFG facts used to resolve matched while loops. */ + private static final class WhileLoopResolutionCache { + + /** + * A back edge from {@code sourceBlock} to {@code targetBlock}. + * + * @param sourceBlock Source block of the back edge. + * @param targetBlock Target block of the back edge. + */ + private record BackEdge(Block sourceBlock, Block targetBlock) { + + /** + * Creates a CFG back-edge description. + * + * @param sourceBlock source block of the back edge + * @param targetBlock target block of the back edge + */ + private BackEdge {} + } + + /** Reachable CFG blocks in the current method. */ + private final Set reachableBlocks; + + /** Back edges among {@link #reachableBlocks}. */ + private final List backEdges; + + /** Natural loops for back edges, computed lazily. */ + private final IdentityHashMap> naturalLoopsByBackEdge = + new IdentityHashMap<>(); + + /** + * Creates CFG facts for resolving matched while loops in the given CFG. + * + * @param cfg the enclosing method CFG + */ + private WhileLoopResolutionCache(ControlFlowGraph cfg) { + Block entryBlock = cfg.getEntryBlock(); + this.reachableBlocks = CollectionOwnershipUtils.reachableFrom(entryBlock); + Map> dominators = computeDominators(entryBlock, reachableBlocks); + this.backEdges = findBackEdges(reachableBlocks, dominators); + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. + * + * @return the CFG back edges + */ + private List getBackEdges() { + return backEdges; + } + + /** + * Returns the natural loop induced by the given back edge, computing it lazily if needed. + * + * @param backEdge the back edge + * @return the natural loop induced by the given back edge + */ + private Set getNaturalLoopForBackEdge(BackEdge backEdge) { + return naturalLoopsByBackEdge.computeIfAbsent( + backEdge, + ignored -> naturalLoop(backEdge.sourceBlock, backEdge.targetBlock, reachableBlocks)); + } + + /** + * Computes dominators for the reachable blocks in the current CFG. + * + * @param entryBlock the CFG entry block + * @param reachableBlocks reachable blocks in the CFG + * @return dominators for each reachable block + */ + private static Map> computeDominators( + Block entryBlock, Set reachableBlocks) { + Map> dominators = new HashMap<>(); + + for (Block block : reachableBlocks) { + if (block.equals(entryBlock)) { + dominators.put(block, new HashSet<>(Set.of(entryBlock))); + } else { + dominators.put(block, new HashSet<>(reachableBlocks)); + } + } + + boolean changed; + do { + changed = false; + for (Block block : reachableBlocks) { + if (block.equals(entryBlock)) { + continue; + } + + Set newDominators = null; + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + Set predecessorDominators = dominators.get(predecessor); + if (predecessorDominators == null) { + continue; + } + if (newDominators == null) { + newDominators = new HashSet<>(predecessorDominators); + } else { + newDominators.retainAll(predecessorDominators); + } + } + + if (newDominators == null) { + newDominators = new HashSet<>(); + } + newDominators.add(block); + + if (!newDominators.equals(dominators.get(block))) { + dominators.put(block, newDominators); + changed = true; + } + } + } while (changed); + + return dominators; + } + + /** + * Returns the back edges among the reachable blocks in the current CFG. The edge A -> B is a + * back edge if B dominates A. + * + * @param reachableBlocks reachable blocks in the CFG + * @param dominators dominators for each reachable block + * @return the CFG back edges + */ + private static List findBackEdges( + Set reachableBlocks, Map> dominators) { + List backEdges = new ArrayList<>(); + for (Block sourceBlock : reachableBlocks) { + for (Block targetBlock : sourceBlock.getSuccessors()) { + if (targetBlock == null || !reachableBlocks.contains(targetBlock)) { + continue; + } + Set sourceDominators = dominators.get(sourceBlock); + if (sourceDominators != null && sourceDominators.contains(targetBlock)) { + backEdges.add(new BackEdge(sourceBlock, targetBlock)); + } + } + } + return backEdges; + } + + /** + * Returns the natural loop induced by the back edge {@code sourceBlock -> targetBlock}. + * + * @param sourceBlock the source of the back edge + * @param targetBlock the target of the back edge + * @param reachableBlocks reachable blocks in the CFG + * @return the natural loop induced by the back edge + */ + private static Set naturalLoop( + Block sourceBlock, Block targetBlock, Set reachableBlocks) { + Set loopBlocks = new HashSet<>(); + ArrayDeque stack = new ArrayDeque<>(); + + loopBlocks.add(targetBlock); + if (loopBlocks.add(sourceBlock)) { + stack.push(sourceBlock); + } + + while (!stack.isEmpty()) { + Block block = stack.pop(); + for (Block predecessor : block.getPredecessors()) { + if (predecessor == null || !reachableBlocks.contains(predecessor)) { + continue; + } + if (loopBlocks.add(predecessor) && !predecessor.equals(targetBlock)) { + stack.push(predecessor); + } + } + } + return loopBlocks; + } + } } From 9a9a714e398b90bdb1f1d246b5e7e50852dfe1c9 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Fri, 8 May 2026 13:15:44 -0700 Subject: [PATCH 372/374] resolves code review comments. --- .../collectionownership/CollectionOwnershipTransfer.java | 2 ++ .../EnhancedForDisposalLoopResolver.java | 6 ------ .../collectionownership/IndexedForDisposalLoopMatcher.java | 4 ++-- .../checker/resourceleak/MustCallConsistencyAnalyzer.java | 6 +++--- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java index 6436762ac1a4..ed987acaf42e 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipTransfer.java @@ -73,6 +73,7 @@ public CollectionOwnershipTransfer( @Override public TransferResult visitAssignment( AssignmentNode node, TransferInput in) { + // Transfer ownership from the rhs to the lhs. TransferResult res = super.visitAssignment(node, in); Node lhs = getNodeOrTempVar(node.getTarget()); @@ -135,6 +136,7 @@ private TransferResult updateStoreForDisposal List requiredElementMethods = atypeFactory.getMustCallValuesOfResourceCollectionComponent(collectionExpression); if (disposalLoopCalledMethods != null + && requiredElementMethods != null && disposalLoopCalledMethods.containsAll(requiredElementMethods)) { elseStore.clearValue(collectionJE); elseStore.insertValue(collectionJE, atypeFactory.OWNINGCOLLECTIONWITHOUTOBLIGATION); diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java index 22829231ebff..9e15106dfe23 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java @@ -227,12 +227,6 @@ private boolean isTargetEnhancedForInvocation( } EnhancedForLoopTree loop = methodInvocationNode.getEnhancedForLoop(); - if (loop == null) { - throw new BugInCF( - "MethodInvocationNode.iterableExpression should be non-null iff" - + " MethodInvocationNode.enhancedForLoop is non-null"); - } - return loop == tree; } } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index d36882824e40..b5432b8b48af 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -340,7 +340,7 @@ public Void visitClass(ClassTree tree, Void p) { @Override public @Nullable Void scan(@Nullable Tree tree, Void p) { // Short-circuit the scanner if the collection/index variable is mutated. - if (bodyIsIllegal || tree == null) { + if (bodyIsIllegal) { return null; } return super.scan(tree, p); @@ -348,7 +348,7 @@ public Void visitClass(ClassTree tree, Void p) { @Override public @Nullable Void scan(@Nullable Iterable trees, Void p) { - if (bodyIsIllegal || trees == null) { + if (bodyIsIllegal) { return null; } return super.scan(trees, p); diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index cb34f16ff4df..3aa2431f9a4c 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -179,7 +179,7 @@ public class MustCallConsistencyAnalyzer { * True if currently executing a loop body analysis and false if executing a normal consistency * analysis. */ - private boolean isLoopBodyAnalysis; + private final boolean isLoopBodyAnalysis; /** * A cache for the result of calling {@code RLCCalledMethodsAnnotatedTypeFactory.getStoreAfter()} @@ -221,7 +221,7 @@ public enum MethodExitKind { } /** - * CFG currently being analyzed by {@link #analyze(ControlFlowGraph)}. + * The CFG currently being analyzed by {@link #analyze(ControlFlowGraph)}. * *

This field is initialized at the start of {@code analyze} and is only used by helpers that * reason about whether newly-created obligations can still reach the regular exit. @@ -239,7 +239,7 @@ public enum MethodExitKind { private @Nullable Set blocksThatCanReachRegularExit = null; /** - * Trees for which collection.obligation.never.enforced has already been reported in the current + * Trees for which `collection.obligation.never.enforced` has already been reported in the current * CFG analysis. * *

This suppresses duplicate diagnostics when the same invocation is visited on multiple From dadd554c55403b8b2fc9938ff4d3641184a59506 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Wed, 13 May 2026 14:33:29 -0700 Subject: [PATCH 373/374] resolves code review comments. --- .../CollectionOwnershipUtils.java | 4 +- .../collectionownership/DisposalLoopInfo.java | 56 +++++++------------ .../DisposalLoopScanner.java | 22 ++++---- ...va => EnhancedForDisposalLoopMatcher.java} | 11 ++-- .../IndexedForDisposalLoopMatcher.java | 4 +- .../WhileDisposalLoopMatcher.java | 31 ++-------- 6 files changed, 48 insertions(+), 80 deletions(-) rename checker/src/main/java/org/checkerframework/checker/collectionownership/{EnhancedForDisposalLoopResolver.java => EnhancedForDisposalLoopMatcher.java} (96%) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java index 64ab76ae080a..1fdbe84859a3 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipUtils.java @@ -149,7 +149,7 @@ static Name getNameFromStatementTree(StatementTree expr) { * @param expr an expression * @return the expression that directly identifies the referenced value, or {@code null} */ - static ExpressionTree baseExpression(ExpressionTree expr) { + static ExpressionTree referenceExpression(ExpressionTree expr) { switch (expr.getKind()) { case IDENTIFIER -> { return expr; @@ -166,7 +166,7 @@ static ExpressionTree baseExpression(ExpressionTree expr) { } } case METHOD_INVOCATION -> { - return baseExpression(((MethodInvocationTree) expr).getMethodSelect()); + return referenceExpression(((MethodInvocationTree) expr).getMethodSelect()); } default -> { return null; diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java index 94603327789f..f5d8923ea7eb 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopInfo.java @@ -2,16 +2,30 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; -import java.util.Objects; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.node.Node; /** - * Stores the resolved CFG and AST facts for a potential disposal loop. A disposal loop is a loop - * that iterates over a resource collection and may call the disposal method, e.g., close() on the - * iterated resource. This record stores the metadata for disposal loops which may or may not - * fullfill the must-call obligations of the iterated element. + * Stores the resolved CFG and AST facts for a potential disposal loop. + * + *

A potential disposal loop is a loop that iterates over a resource collection and whose shape + * matches one of the supported collection iteration patterns recognized by {@link + * DisposalLoopScanner}. This record stores the metadata for such loops which may or may not fulfill + * the must-call obligations of the iterated element. + * + *

A potential disposal loop becomes a certified disposal loop only after later analysis + * determines that the loop calls methods on the iterated element to satisfy its {@code @MustCall} + * obligations. That process starts in {@link + * CollectionOwnershipAnnotatedTypeFactory#postCFGConstruction(ControlFlowGraph, UnderlyingAST)}, + * which calls {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer#analyzeDisposalLoop( + * ControlFlowGraph, DisposalLoopInfo)} to compute the definitely called methods on the iterated + * element. During later collection-ownership refinement, if those called methods satisfy the + * iterated element's {@code @MustCall} obligations, the loop is treated as a certified disposal + * loop. * * @param expressionTree The {@code ExpressionTree} for collection that this loop iterates over. * @param iteratedElementTree The {@code Tree} for the iterated collection element by this loop. @@ -28,34 +42,4 @@ public record DisposalLoopInfo( Tree loopConditionTree, ConditionalBlock loopConditionalBlock, Block loopBodyEntryBlock, - Block loopUpdateBlock) { - - /** - * Constructs a new {@code DisposalLoopInfo}. - * - * @param expressionTree the {@code ExpressionTree} for the collection that this loop iterates - * over - * @param iteratedElementTree the {@code Tree} for the iterated collection element - * @param iteratedElementNode the CFG {@code Node} for the iterated collection element - * @param loopConditionTree the condition {@code Tree} for this loop - * @param loopConditionalBlock the conditional {@code Block} corresponding to the loop condition - * @param loopBodyEntryBlock the entry {@code Block} for this loop's body - * @param loopUpdateBlock the loop-update {@code Block} - */ - public DisposalLoopInfo( - ExpressionTree expressionTree, - Tree iteratedElementTree, - Node iteratedElementNode, - Tree loopConditionTree, - ConditionalBlock loopConditionalBlock, - Block loopBodyEntryBlock, - Block loopUpdateBlock) { - this.expressionTree = Objects.requireNonNull(expressionTree); - this.iteratedElementTree = Objects.requireNonNull(iteratedElementTree); - this.iteratedElementNode = Objects.requireNonNull(iteratedElementNode); - this.loopConditionTree = Objects.requireNonNull(loopConditionTree); - this.loopConditionalBlock = Objects.requireNonNull(loopConditionalBlock); - this.loopBodyEntryBlock = Objects.requireNonNull(loopBodyEntryBlock); - this.loopUpdateBlock = Objects.requireNonNull(loopUpdateBlock); - } -} + Block loopUpdateBlock) {} diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java index 91c3d75cf431..8c1bf1c8eb9a 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/DisposalLoopScanner.java @@ -4,7 +4,7 @@ import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.Tree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.WhileLoopTree; import com.sun.source.util.TreeScanner; import java.util.LinkedHashSet; @@ -12,7 +12,10 @@ import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.dataflow.cfg.ControlFlowGraph; -/** Scans one method tree and discovers {@link DisposalLoopInfo}'s in it. */ +/** + * Scans one method tree and discovers potential disposal loops (represented with {@link + * DisposalLoopInfo} objects) in it. + */ public class DisposalLoopScanner extends TreeScanner { /** The CO type factory used for collection-ownership queries. */ @@ -33,8 +36,8 @@ public class DisposalLoopScanner extends TreeScanner { /** Matcher for `while` disposal loops. */ private final WhileDisposalLoopMatcher whileDisposalLoopMatcher; - /** Resolver for enhanced-`for` disposal loops. */ - private final EnhancedForDisposalLoopResolver enhancedForDisposalLoopResolver; + /** Matcher for enhanced-`for` disposal loops. */ + private final EnhancedForDisposalLoopMatcher enhancedForDisposalLoopMatcher; /** * Creates a scanner for disposal loops in one method CFG. @@ -53,17 +56,16 @@ public DisposalLoopScanner( this.indexedForDisposalLoopMatcher = new IndexedForDisposalLoopMatcher(this.coAtf, this.cfg); this.whileDisposalLoopMatcher = new WhileDisposalLoopMatcher(this.coAtf, this.rlccAtf, this.cfg); - this.enhancedForDisposalLoopResolver = - new EnhancedForDisposalLoopResolver(this.coAtf, this.cfg); + this.enhancedForDisposalLoopMatcher = new EnhancedForDisposalLoopMatcher(this.coAtf, this.cfg); } /** - * Scans a tree and returns the disposal loops discovered in it. + * Scans a method tree and returns the disposal loops discovered in it. * - * @param tree the tree to scan + * @param tree the method tree to scan * @return the disposal loops discovered in {@code tree} */ - public Set scanTree(Tree tree) { + public Set scanTree(MethodTree tree) { scan(tree, null); return disposalLoopInfos; } @@ -135,7 +137,7 @@ public Void visitWhileLoop(WhileLoopTree tree, Void p) { */ @Override public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { - DisposalLoopInfo disposalLoopInfo = enhancedForDisposalLoopResolver.match(tree); + DisposalLoopInfo disposalLoopInfo = enhancedForDisposalLoopMatcher.match(tree); if (disposalLoopInfo != null) { disposalLoopInfos.add(disposalLoopInfo); } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopMatcher.java similarity index 96% rename from checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java rename to checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopMatcher.java index 9e15106dfe23..71c390f3603f 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopResolver.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/EnhancedForDisposalLoopMatcher.java @@ -23,8 +23,8 @@ import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.IPair; -/** Resolves enhanced-`for` {@link DisposalLoopInfo} from CFG. */ -final class EnhancedForDisposalLoopResolver { +/** Matches enhanced-`for` {@link DisposalLoopInfo} from CFG. */ +final class EnhancedForDisposalLoopMatcher { /** The CO type factory used for collection-ownership queries. */ private final CollectionOwnershipAnnotatedTypeFactory coAtf; @@ -33,12 +33,12 @@ final class EnhancedForDisposalLoopResolver { private final ControlFlowGraph cfg; /** - * Creates a resolver for enhanced-`for` disposal loops. + * Creates a matcher for enhanced-`for` disposal loops. * * @param coAtf the CO type factory * @param cfg the CFG being scanned */ - EnhancedForDisposalLoopResolver( + EnhancedForDisposalLoopMatcher( CollectionOwnershipAnnotatedTypeFactory coAtf, ControlFlowGraph cfg) { this.coAtf = coAtf; this.cfg = cfg; @@ -52,7 +52,8 @@ final class EnhancedForDisposalLoopResolver { * @return the matched disposal loop info, or {@code null} if the loop does not match */ @Nullable DisposalLoopInfo match(EnhancedForLoopTree tree) { - ExpressionTree collectionTree = CollectionOwnershipUtils.baseExpression(tree.getExpression()); + ExpressionTree collectionTree = + CollectionOwnershipUtils.referenceExpression(tree.getExpression()); if (collectionTree == null) { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java index b5432b8b48af..3fc5ce972220 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/IndexedForDisposalLoopMatcher.java @@ -110,7 +110,7 @@ final class IndexedForDisposalLoopMatcher { } ExpressionTree collectionExpression = - CollectionOwnershipUtils.baseExpression(collectionElementTree); + CollectionOwnershipUtils.referenceExpression(collectionElementTree); if (collectionExpression == null) { return null; } @@ -238,7 +238,7 @@ private boolean isIthCollectionElement(Tree tree, Name indexVariableName) { /** * Scans an indexed {@code for} loop body, rejecting writes that invalidate the simple - * indexed-loop model and remembers the last matching {@code collection.get(i)} access. + * indexed-loop model and remembering the last matching {@code collection.get(i)} access. */ private final class LoopBodyScanner extends TreeScanner { diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index 03c421ec92d3..943733187642 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -27,6 +27,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.rlccalledmethods.RLCCalledMethodsAnnotatedTypeFactory; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -71,7 +72,7 @@ final class WhileDisposalLoopMatcher { private final ControlFlowGraph cfg; /** Lazily-computed CFG facts for while-loop resolution. */ - private @Nullable WhileLoopResolutionCache whileLoopCache; + private @MonotonicNonNull WhileLoopResolutionCache whileLoopCache; /** * Creates a matcher for {@code while} disposal loops. @@ -332,7 +333,7 @@ final class WhileDisposalLoopMatcher { return null; } - ExpressionTree collectionTree = CollectionOwnershipUtils.baseExpression(receiver); + ExpressionTree collectionTree = CollectionOwnershipUtils.referenceExpression(receiver); if (collectionTree == null) { return null; } @@ -413,7 +414,7 @@ private boolean isIsEmptyCall(MethodInvocationTree invocation) { if (!coAtf.isResourceCollection(collectionExpression)) { return null; } - return CollectionOwnershipUtils.baseExpression(collectionExpression); + return CollectionOwnershipUtils.referenceExpression(collectionExpression); } /** @@ -744,18 +745,7 @@ private record WhileHeaderInfo( ExpressionTree collectionTree, @Nullable Name collectionVarNameForInvalidation, Name headerVarName, - Set allowedExtractMethods) { - - /** - * Creates a summary of the facts recovered from a matched while-loop header. - * - * @param collectionTree the owning collection expression to mark - * @param collectionVarNameForInvalidation collection variable whose writes invalidate the match - * @param headerVarName iterator or collection variable constrained by the header - * @param allowedExtractMethods extraction methods accepted for this matched header - */ - private WhileHeaderInfo {} - } + Set allowedExtractMethods) {} /** Lazily-computed CFG facts used to resolve matched while loops. */ private static final class WhileLoopResolutionCache { @@ -766,16 +756,7 @@ private static final class WhileLoopResolutionCache { * @param sourceBlock Source block of the back edge. * @param targetBlock Target block of the back edge. */ - private record BackEdge(Block sourceBlock, Block targetBlock) { - - /** - * Creates a CFG back-edge description. - * - * @param sourceBlock source block of the back edge - * @param targetBlock target block of the back edge - */ - private BackEdge {} - } + private record BackEdge(Block sourceBlock, Block targetBlock) {} /** Reachable CFG blocks in the current method. */ private final Set reachableBlocks; From 1f3badfe14f55501707170acabdb927c771378e7 Mon Sep 17 00:00:00 2001 From: Sanjay Malakar Date: Wed, 13 May 2026 18:16:35 -0700 Subject: [PATCH 374/374] resolves code review comments. --- ...llectionOwnershipAnnotatedTypeFactory.java | 8 ++--- .../WhileDisposalLoopMatcher.java | 13 ++++++-- .../resourceleak/ResourceLeakUtils.java | 33 +------------------ .../RLCCalledMethodsAnnotatedTypeFactory.java | 12 ------- 4 files changed, 13 insertions(+), 53 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java index e10a122f6189..4eb1418aaa31 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java @@ -13,7 +13,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -106,7 +105,7 @@ public class CollectionOwnershipAnnotatedTypeFactory * Map from a {@code MethodTree} to the {@link DisposalLoopInfo}s discovered in that method's * body. */ - private final IdentityHashMap> + private final IdentityHashMap> preparedDisposalLoopInfosByMethod = new IdentityHashMap<>(); /** The {@code @}{@link NotOwningCollection} annotation. */ @@ -121,10 +120,7 @@ public class CollectionOwnershipAnnotatedTypeFactory /** The {@code @}{@link OwningCollectionWithoutObligation} annotation. */ public final AnnotationMirror OWNINGCOLLECTIONWITHOUTOBLIGATION; - /** - * The {@code @}{@link OwningCollectionBottom}{@code ()} annotation. It is the default in - * unannotated code. - */ + /** The {@code @}{@link OwningCollectionBottom}{@code ()} annotation. */ public final AnnotationMirror BOTTOM; /** The {@code @}{@link PolyOwningCollection}{@code ()} polymorphic annotation. */ diff --git a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java index 943733187642..cfd07f7aa94b 100644 --- a/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java +++ b/checker/src/main/java/org/checkerframework/checker/collectionownership/WhileDisposalLoopMatcher.java @@ -317,8 +317,9 @@ final class WhileDisposalLoopMatcher { } /** - * Builds a {@link WhileHeaderInfo} for a non-empty collection header once the receiver expression - * has already been recovered. + * Builds a {@link WhileHeaderInfo} once {@link #matchNonEmptyFromExpr(ExpressionTree)} or {@link + * #matchNonEmptyFromSize(BinaryTree)} has already matched a non-empty collection header condition + * such as {@code !c.isEmpty()}, {@code c.size() > 0}, or {@code 0 < c.size()}. * * @param receiver the receiver checked by the non-empty header * @return the recovered header facts, or {@code null} if the receiver is not a resource @@ -802,7 +803,13 @@ private Set getNaturalLoopForBackEdge(BackEdge backEdge) { } /** - * Computes dominators for the reachable blocks in the current CFG. + * Computes dominators for the reachable blocks in the current CFG using the standard iterative + * dataflow equations: + * + *

+     *  Dom(entry) = {entry}
+     *  Dom(n) = {n} ∪ ⋂ Dom(p)    for each other reachable block n
+     * 
* * @param entryBlock the CFG entry block * @param reachableBlocks reachable blocks in the CFG diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java index e2782d9e946e..e11a50c8bf65 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.resourceleak; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -44,16 +43,6 @@ private ResourceLeakUtils() { throw new Error("Do not instantiate"); } - /** List of checker names associated with the Resource Leak Checker. */ - public static List rlcCheckers = - new ArrayList<>( - Arrays.asList( - ResourceLeakChecker.class.getCanonicalName(), - CollectionOwnershipChecker.class.getCanonicalName(), - RLCCalledMethodsChecker.class.getCanonicalName(), - MustCallChecker.class.getCanonicalName(), - MustCallNoCreatesMustCallForChecker.class.getCanonicalName())); - /** * Given a type factory that is part of the resource leak checker hierarchy, returns the {@link * ResourceLeakChecker} in the checker hierarchy. @@ -88,22 +77,6 @@ public static ResourceLeakChecker getResourceLeakChecker(SourceChecker reference } } - /** - * Given a type factory part of the resource leak ecosystem, returns the {@link - * MustCallAnnotatedTypeFactory} in the checker hierarchy. - * - * @param referenceAtf the type factory to retrieve the {@link MustCallAnnotatedTypeFactory} from - * @return the {@link MustCallAnnotatedTypeFactory} in the checker hierarchy - */ - public static MustCallAnnotatedTypeFactory getMustCallAnnotatedTypeFactory( - AnnotatedTypeFactory referenceAtf) { - if (referenceAtf == null) { - throw new IllegalArgumentException("Argument referenceAtf cannot be null"); - } else { - return getMustCallAnnotatedTypeFactory(referenceAtf.getChecker()); - } - } - /** * Given a checker part of the resource leak ecosystem, returns the {@link * MustCallAnnotatedTypeFactory} in the checker hierarchy. @@ -254,11 +227,7 @@ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnno */ public static CollectionOwnershipAnnotatedTypeFactory getCollectionOwnershipAnnotatedTypeFactory( AnnotatedTypeFactory referenceAtf) { - if (referenceAtf == null) { - throw new IllegalArgumentException("Argument referenceAtf cannot be null"); - } else { - return getCollectionOwnershipAnnotatedTypeFactory(referenceAtf.getChecker()); - } + return getCollectionOwnershipAnnotatedTypeFactory(referenceAtf.getChecker()); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java index 6bee059796ae..92fbedfae3c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java @@ -428,18 +428,6 @@ public boolean canCreateObligations() { return !rlc.hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); } - /** - * Fetches the store from the results of dataflow for {@code block}. The store after {@code block} - * is returned. - * - * @param block a block - * @return the appropriate CFStore, populated with CalledMethods annotations, from the results of - * running dataflow - */ - public AccumulationStore getStoreAfterBlock(Block block) { - return flowResult.getStoreAfter(block); - } - /** * Returns the then or else store after {@code block} depending on the value of {@code then} is * returned.