Skip to content

Conversation

@skehrli
Copy link
Contributor

@skehrli skehrli commented Jul 23, 2025

Summary by CodeRabbit

  • New Features

    • Introduces collection-ownership checking integrated with the Resource Leak Checker, with new annotations (e.g., owning/non-owning collections and field destructors).
    • Defaults and ownership transfer for parameters, returns, fields, assignments, and new allocations.
    • Loop-aware analysis (enhanced-for and index loops) to verify element must-call obligations.
    • Improved diagnostics and dataflow visualization for iterated collection elements.
  • Bug Fixes

    • Fewer false positives: iterator and map access now reflect non-owning returns, reducing spurious errors.
  • Documentation

    • Manual updated with collection-ownership model, defaults, and usage guidance.
  • Tests

    • Extensive new test suites for collections, fields, loops, and defaults.

Sascha Kehrli and others added 30 commits June 20, 2025 13:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines 1511 to 1517
/**
* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider annotating push and get for completeness.

To keep the API surface consistent, annotate push/get receivers as @NotOwningCollection too.

-  public ImmutableStack<E> push(E elem) {
+  public ImmutableStack<E> push(@NotOwningCollection ImmutableStack<E> this, E elem) {
     return extend(elem, this);
   }
@@
-  public E get(int index) {
+  public E get(@NotOwningCollection ImmutableStack<E> this, int index) {
     int n = size();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 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) {
public ImmutableStack<E> push(@NotOwningCollection ImmutableStack<E> this, E elem) {
return extend(elem, this);
}
/**
* 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(@NotOwningCollection ImmutableStack<E> this, int index) {
int n = size();
// …rest of method unchanged…
🤖 Prompt for AI Agents
In
annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java
around lines 1511-1517, the get method (and the push method elsewhere in this
class) should have their receivers annotated as @NotOwningCollection to keep the
API consistent; update both method signatures to add the receiver annotation
(e.g., change "public E get(int index)" to include the receiver annotation on
the implicit this, and likewise add @NotOwningCollection to the push method's
receiver) so the methods are explicitly marked as not owning the collection.

Comment on lines 23 to 26
/** The map that backs this set. */
// Declared as LinkedHashMap because some implementations of Map prohibit null keys.
private final LinkedHashMap<K, V> theMap = new LinkedHashMap<>();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer interface type for the field to reduce coupling

Declaring theMap as Map preserves flexibility; the runtime implementation remains LinkedHashMap. The null-keys rationale doesn’t require the declared type to be LinkedHashMap.

-import java.util.LinkedHashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
@@
-  /** The map that backs this set. */
-  // Declared as LinkedHashMap because some implementations of Map prohibit null keys.
-  private final LinkedHashMap<K, V> theMap = new LinkedHashMap<>();
+  /** The map that backs this set (insertion-ordered). */
+  private final Map<K, V> theMap = new LinkedHashMap<>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** The map that backs this set. */
// Declared as LinkedHashMap because some implementations of Map prohibit null keys.
private final LinkedHashMap<K, V> theMap = new LinkedHashMap<>();
// --- imports ---
import java.util.LinkedHashMap;
import java.util.Map;
// --- inside the class ---
/** The map that backs this set (insertion-ordered). */
private final Map<K, V> theMap = new LinkedHashMap<>();
🤖 Prompt for AI Agents
In
annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java
around lines 23 to 26, the field is declared as a concrete LinkedHashMap which
increases coupling; change the field declaration to use the Map interface (e.g.,
private final Map<K, V> theMap = new LinkedHashMap<>();) while keeping the
runtime implementation as LinkedHashMap, retain or update the comment about
null-key behavior, and verify no code elsewhere relies on LinkedHashMap-specific
APIs—if it does, adjust those usages or narrow the type only where necessary.

Comment on lines +39 to +44
public CollectionOwnershipStore(
CollectionOwnershipAnalysis analysis,
CFAbstractStore<CFValue, CollectionOwnershipStore> other) {
super(other);
this.atypeFactory = ((CollectionOwnershipStore) other).atypeFactory;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Avoid brittle downcast in copy ctor

Derive the type factory from the analysis to prevent accidental ClassCastException if other isn’t a CollectionOwnershipStore.

   public CollectionOwnershipStore(
       CollectionOwnershipAnalysis analysis,
       CFAbstractStore<CFValue, CollectionOwnershipStore> other) {
     super(other);
-    this.atypeFactory = ((CollectionOwnershipStore) other).atypeFactory;
+    this.atypeFactory =
+        (CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public CollectionOwnershipStore(
CollectionOwnershipAnalysis analysis,
CFAbstractStore<CFValue, CollectionOwnershipStore> other) {
super(other);
this.atypeFactory = ((CollectionOwnershipStore) other).atypeFactory;
}
public CollectionOwnershipStore(
CollectionOwnershipAnalysis analysis,
CFAbstractStore<CFValue, CollectionOwnershipStore> other) {
super(other);
this.atypeFactory =
(CollectionOwnershipAnnotatedTypeFactory) analysis.getTypeFactory();
}
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java
around lines 39 to 44, the copy constructor currently downcasts `other` to
`CollectionOwnershipStore` to obtain `atypeFactory`, which is brittle and can
cause ClassCastException; instead, remove the cast and derive the type factory
from the provided `analysis` (e.g., call a getter like
`analysis.getTypeFactory()` or `analysis.getATypeFactory()` or access the
appropriate field on `analysis`) and assign that to `this.atypeFactory`, keeping
the rest of the constructor unchanged.

Comment on lines +49 to +61
@Override
protected CFValue newFieldValueAfterMethodCall(
FieldAccess fieldAccess,
GenericAnnotatedTypeFactory<CFValue, CollectionOwnershipStore, ?, ?> atf,
CFValue value) {
CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value);
if (superResult == null) {
if (atypeFactory.isResourceCollectionField(fieldAccess.getField())) {
return value;
}
}
return superResult;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t reintroduce null field values

If value is null, returning it defeats the “keep in store” intent. Guard on value != null.

   @Override
   protected CFValue newFieldValueAfterMethodCall(
       FieldAccess fieldAccess,
       GenericAnnotatedTypeFactory<CFValue, CollectionOwnershipStore, ?, ?> atf,
       CFValue value) {
     CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value);
-    if (superResult == null) {
-      if (atypeFactory.isResourceCollectionField(fieldAccess.getField())) {
-        return value;
-      }
-    }
-    return superResult;
+    if (superResult == null
+        && value != null
+        && atypeFactory.isResourceCollectionField(fieldAccess.getField())) {
+      return value;
+    }
+    return superResult;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
protected CFValue newFieldValueAfterMethodCall(
FieldAccess fieldAccess,
GenericAnnotatedTypeFactory<CFValue, CollectionOwnershipStore, ?, ?> atf,
CFValue value) {
CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value);
if (superResult == null) {
if (atypeFactory.isResourceCollectionField(fieldAccess.getField())) {
return value;
}
}
return superResult;
}
@Override
protected CFValue newFieldValueAfterMethodCall(
FieldAccess fieldAccess,
GenericAnnotatedTypeFactory<CFValue, CollectionOwnershipStore, ?, ?> atf,
CFValue value) {
CFValue superResult = super.newFieldValueAfterMethodCall(fieldAccess, atf, value);
if (superResult == null
&& value != null
&& atypeFactory.isResourceCollectionField(fieldAccess.getField())) {
return value;
}
return superResult;
}
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java
around lines 49 to 61, the method newFieldValueAfterMethodCall returns the
incoming value when the superclass result is null and the field is a resource
collection field; this reintroduces nulls if value is null. Update the guard to
only return value when value != null (i.e., check
atypeFactory.isResourceCollectionField(...) && value != null) and otherwise
return superResult so null values are not reintroduced into the store.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +128 to +136
private static Map<Tree, PotentiallyFulfillingLoop> 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<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap =
new HashMap<>();
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Thread safety concern with static mutable maps.

The static maps conditionToFulfillingLoopMap and conditionalBlockToFulfillingLoopMap are mutable and accessed without synchronization. This could lead to race conditions if the checker is used in a multi-threaded environment.

Consider using concurrent collections or adding synchronization:

-private static Map<Tree, PotentiallyFulfillingLoop> conditionToFulfillingLoopMap =
-    new HashMap<>();
+private static final Map<Tree, PotentiallyFulfillingLoop> conditionToFulfillingLoopMap =
+    new ConcurrentHashMap<>();

-private static Map<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap =
-    new HashMap<>();
+private static final Map<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap =
+    new ConcurrentHashMap<>();
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java
around lines 128 to 136, the two static mutable maps are not thread-safe and can
race; replace their declarations with thread-safe alternatives (e.g., make them
ConcurrentHashMap instances) or convert them to non-static instance fields or
thread-local storage if per-factory state is intended; ensure all accesses use
the chosen thread-safe structure (or synchronize accessors) so reads/writes
cannot race.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the agent's complaint here is reasonable: I don't see a reason for these maps to be static. However, the fix that the agent is proposing seems like overkill: why synchronize them when we could instead make these instance fields? There shouldn't ever be more than one ATF per running checker.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kelloggm Thanks — agreed. I’ll switch these to instance fields and drop the static helpers so there’s no shared mutable state across analyses.

Proposed changes:

  1. Make the maps instance-scoped and final
// In CollectionOwnershipAnnotatedTypeFactory.java

// before:
// private static Map<Tree, PotentiallyFulfillingLoop> conditionToFulfillingLoopMap = new HashMap<>();
// private static Map<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap = new HashMap<>();

// after:
private final Map<Tree, PotentiallyFulfillingLoop> conditionToFulfillingLoopMap = new HashMap<>();
private final Map<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap = new HashMap<>();
  1. Convert the helper methods to instance methods
// before (static):
// public static void markFulfillingLoop(PotentiallyFulfillingLoop loop) { ... }
// public static @Nullable PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) { ... }
// public static @Nullable PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Block block) { ... }

// after (instance):
public void markFulfillingLoop(PotentiallyFulfillingLoop loop) {
  conditionToFulfillingLoopMap.put(loop.condition, loop);
  conditionalBlockToFulfillingLoopMap.put(loop.loopConditionalBlock, loop);
}

public @org.checkerframework.checker.nullness.qual.Nullable
PotentiallyFulfillingLoop getFulfillingLoopForCondition(Tree tree) {
  return conditionToFulfillingLoopMap.get(tree);
}

public @org.checkerframework.checker.nullness.qual.Nullable
PotentiallyFulfillingLoop getFulfillingLoopForConditionalBlock(Block block) {
  return conditionalBlockToFulfillingLoopMap.get(block);
}
  1. Optional hygiene: clear per-CFG to prevent stale entries
@Override
public void postAnalyze(ControlFlowGraph cfg) {
  super.postAnalyze(cfg);
  conditionToFulfillingLoopMap.clear();
  conditionalBlockToFulfillingLoopMap.clear();
}
  1. Update call sites to use the ATF instance
  • From within collection-ownership code (e.g., CollectionOwnershipTransfer):
CollectionOwnershipAnnotatedTypeFactory coAtf = (CollectionOwnershipAnnotatedTypeFactory) atypeFactory;
PotentiallyFulfillingLoop pfl = coAtf.getFulfillingLoopForCondition(conditionTree);
  • From other checkers (e.g., MustCallConsistencyAnalyzer) via utilities already in this PR:
CollectionOwnershipAnnotatedTypeFactory coAtf =
    org.checkerframework.checker.resourceleak.ResourceLeakUtils
        .getCollectionOwnershipAnnotatedTypeFactory(mcAtf /* or a reference ATF/checker */);
coAtf.markFulfillingLoop(loop);

If you’re happy with this direction, I’ll prepare the follow-up edits to method signatures and adjust the few call sites accordingly; CI should go green after these changes since the usage becomes explicit and state stays within the factory instance.


✏️ Learnings added
Learnt from: kelloggm
PR: typetools/checker-framework#7166
File: checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java:128-136
Timestamp: 2025-10-22T20:41:01.096Z
Learning: In checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java, prefer instance fields over static state for analysis caches (e.g., maps keyed by Tree/Block). There is at most one ATF per running checker, so static mutable fields are discouraged.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
docs/manual/resource-leak-checker.tex (4)

659-667: Definition excludes arrays but examples include them.

The definition of "resource collection" restricts it to java.util.Iterable implementations (line 662), but the example on line 666 includes Socket[], which is an array type and does not implement Iterable. This inconsistency was previously flagged.

Either:

  • Update the definition to explicitly include array types alongside Iterable, or
  • Remove Socket[] from the examples and clarify that arrays are not supported.

Based on the context, arrays appear to be intended as resource collections (see indexed loop support in lines 794-817), so the definition should be broadened.


794-817: Indexed loop requirements are List-specific; clarify scope or add array support.

The indexed loop requirements mandate c.size() (line 800) and collection.get(i) (line 802), which are List-specific constructs. However, the earlier definition includes arrays as resource collections (line 666). This creates an inconsistency: if arrays are resource collections, indexed loops should support them with array.length and array[i].

Apply one of:

  • Option A: Scope this section explicitly to List by adding "For java.util.List," before "Loop iteration bounds:" and clarifying arrays are unsupported here.
  • Option B: Extend the rules to include arrays by accepting array.length as upper bound and array[i] for element access.

The previous review suggested the same fix.


823-823: Fix: interface requires implements, not extends.

Closeable is an interface, so the declaration must use implements:

-class Aggregator extends Closeable {
+class Aggregator implements Closeable {

This was flagged in a previous review.


873-873: Spelling: "transfering" → "transferring".

-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.
+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.

This was previously noted.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49a566f and 50ff472.

📒 Files selected for processing (1)
  • docs/manual/resource-leak-checker.tex (2 hunks)
🔇 Additional comments (3)
docs/manual/resource-leak-checker.tex (3)

138-158: LGTM! Clear introduction to collection ownership annotations.

The new subsection properly introduces the collection ownership annotations and the @CollectionFieldDestructor annotation with appropriate forward references to detailed sections.


668-688: LGTM! Clear and accurate type hierarchy.

The ownership type system hierarchy is well-presented and the descriptions of each qualifier are technically accurate.


690-766: LGTM! Comprehensive explanation of ownership transfer semantics.

The examples clearly illustrate how ownership defaults work, how transfer occurs through parameters, returns, and assignments, and how obligations move with ownership.

Comment on lines 889 to 956
% \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 \<ConcurrentModificationException> 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<Socket> 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<Socket> foo(@OwningCollection List<Socket> list) {
% Iterator<Socket> 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<Socket> foo(@OwningCollection List<Socket> list) {
% Iterator<Socket> 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<Socket> foo(@OwningCollection List<Socket> list) {
% Iterator<Socket> 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Large commented-out section on iterators.

This 67-line block describes iterator support but is entirely commented out. If iterator support is implemented, uncomment and finalize this section. If not yet ready, consider removing it or adding a TODO comment explaining its status.

🤖 Prompt for AI Agents
docs/manual/resource-leak-checker.tex lines 889-956 contain a large 67-line
commented-out subsection about iterator support; either fully restore it
(uncomment and finalize the text/examples) if iterator handling is
implemented—ensuring the examples compile, references/labels are correct, and
the docs build cleanly—or remove the block and replace it with a short TODO note
stating iterator support status and next steps; after change, run the
documentation build and update any cross-references or table-of-contents entries
accordingly.

Comment on lines +957 to +959
\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]]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

TODO: Add JDK method signatures table.

The placeholder TODO at line 958 indicates this section is incomplete. The documentation references JDK method signatures for collection ownership but doesn't provide the promised table.

Would you like me to help draft the table structure or open an issue to track completing this section?

🤖 Prompt for AI Agents
In docs/manual/resource-leak-checker.tex around lines 957 to 959, the TODO
placeholder indicates the missing table of JDK collection/iterable method
signatures and their ownership annotations; replace the placeholder with a LaTeX
table that lists method signature, returned/consumed collection type, ownership
annotation (e.g., returnsOwned, borrows, transfers), and a short note/example
column; include common methods from java.util and streams (e.g.,
Collection#iterator, Collection#addAll, List#subList, Stream#collect,
Arrays#asList) with their correct ownership classification and a brief
rationale, and ensure the table is labeled and referenced by the subsection so
cross-references compile.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (13)
docs/manual/resource-leak-checker.tex (4)

659-667: Definition excludes arrays but examples elsewhere use array types.

The definition restricts resource collections to java.util.Iterable implementations (line 662), but other parts of the documentation reference array types like Socket[] as resource collections. This inconsistency was noted in previous reviews.

As per the previous review, either:

  • Explicitly state that arrays are not supported, or
  • Extend the definition to include array types (e.g., T[] where T has non-empty @MustCall type)

Based on past review comments.


797-817: Indexed loop requirements are List-specific; clarify scope or extend to arrays.

The indexed loop requirements (lines 798-802) reference c.size() and collection.get(i), which are java.util.List methods not available on all Iterable implementations. Arrays would require array.length and array[i] instead. This was flagged in previous reviews.

As per the previous review, either:

  • Scope this section explicitly to List (e.g., "For java.util.List, loop iteration bounds:"), or
  • Extend the rules to include arrays with parallel syntax (e.g., "upper bound is c.size() for List or a.length for arrays")

Based on past review comments.


889-956: Large commented-out iterator section remains unresolved.

This 67-line commented-out section on iterator support was flagged in previous reviews. The section should either be:

  • Uncommented and finalized if iterator support is implemented, or
  • Removed and replaced with a brief TODO note explaining iterator support status

As per previous reviews, decide whether to activate or remove this section to avoid carrying stale commented-out documentation.

Based on past review comments.


957-959: TODO placeholder for JDK method signatures table still empty.

The TODO at line 958 was flagged in previous reviews but remains incomplete. The documentation references JDK method signatures for collection ownership but doesn't provide the promised table.

As per previous reviews, this table should list common JDK collection/Iterable methods (e.g., Collection#iterator, List#subList, Stream#collect) with their ownership annotations and brief explanations.

Based on past review comments.

checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java (6)

80-87: Address or track the commented-out test.

The commented-out reassignCollectionFieldIfNull test with TODO should either be re-enabled with expected diagnostics or tracked in an issue to prevent it from being forgotten.

Please clarify: Is this test blocked by pending functionality, or can it be enabled now? If blocked, please create a tracking issue and reference it in the TODO comment.


96-103: Add @OverRide annotation for consistency.

The close() method implements Closeable but is missing the @Override annotation.

Apply this diff:

 @CollectionFieldDestructor("resList")
+@Override
 public void close() {

145-152: Add @OverRide annotation.

Same as above - missing @Override for Closeable.close().

 @CollectionFieldDestructor("fieldList")
+@Override
 public void close() {

163-170: Add @OverRide annotation.

Same as above.

 @CollectionFieldDestructor("fieldList")
+@Override
 public void close() {

176-183: Add @OverRide annotation.

Same as above.

 @CollectionFieldDestructor("fieldList")
+@Override
 public void close() {

193-200: Add @OverRide annotation.

Same as above.

 @CollectionFieldDestructor("fieldList")
+@Override
 public void close() {
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (2)

3516-3517: Use object equality for block comparison.

Reference equality (==) is brittle for comparing CFG blocks. While the suppression indicates this is intentional, using .equals() would be more robust.

Apply this diff:

-@SuppressWarnings("interning:not.interned")
-boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock;
+boolean isLastBlockOfBody = Objects.equals(successorAndExceptionType.first, loopUpdateBlock);

This matches the feedback from previous reviews at lines 3516-3517.


3576-3578: Use object equality for block comparison.

Same issue as line 3516-3517: reference equality should be replaced with object equality.

Apply this diff:

-@SuppressWarnings("interning:not.interned")
-boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock;
+boolean thenSuccessor = Objects.equals(conditionalBlock.getThenSuccessor(), loopUpdateBlock);
checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java (1)

18-21: Fix typo in comment.

The comment contains "resoure" which should be "resource".

Apply this diff:

-   * something is a resoure collection.
+   * something is a resource collection.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50ff472 and 69175cf.

📒 Files selected for processing (6)
  • checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (42 hunks)
  • checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java (1 hunks)
  • checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java (1 hunks)
  • checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java (1 hunks)
  • dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1 hunks)
  • docs/manual/resource-leak-checker.tex (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (6)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java (1)
  • CollectionOwnershipAnnotatedTypeFactory (64-745)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipStore.java (1)
  • CollectionOwnershipStore (16-62)
checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java (2)
  • RLCCalledMethodsAnnotatedTypeFactory (66-749)
  • PotentiallyFulfillingLoop (630-707)
dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java (1)
  • TransferInput (22-314)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)
  • IteratedCollectionElement (13-88)
checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java (1)
  • ResourceLeakUtils (40-468)
🪛 ast-grep (0.39.5)
checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java

[info] 44-44: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 54-54: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket("", 42)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 81-81: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 99-99: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 115-115: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 117-117: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 119-119: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 121-121: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (21)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (4)

32-47: LGTM! equals and hashCode are correctly implemented.

The equals method now correctly uses && for transitivity and calls equals on this-fields. The hashCode method is consistent with equals, hashing both tree and node. Previous review feedback has been properly addressed.


55-58: Verify that unconditionally returning true is correct.

The method always returns true. Ensure this aligns with the intended semantics for iterated collection elements. If the determinism should depend on the underlying node, consider delegating to node or checking its properties.


74-82: LGTM! Correct for loop iteration variables.

Returning false for both isAssignableByOtherCode and isModifiableByOtherCode is appropriate for iteration variables, which are local to the loop scope and cannot be assigned or modified by external code.


49-72: LGTM! Standard implementations for JavaExpression API.

The implementations of containedOfClass, syntacticEquals, containsSyntacticEqualJavaExpression, and accept follow standard patterns for JavaExpression subclasses and are correct.

Also applies to: 84-87

docs/manual/resource-leak-checker.tex (3)

138-158: LGTM: Clear subsection structure and annotation descriptions.

The new subsection introducing collection ownership annotations is well-organized and provides appropriate forward references to detailed sections.


668-766: LGTM: Clear ownership type hierarchy and accurate examples.

The ownership type system hierarchy is well-explained, and the code examples correctly demonstrate:

  • Fresh allocation defaults
  • Type refinement on element addition
  • Ownership transfer through parameters, returns, and assignments

The examples are technically sound and illustrate the concepts effectively.


818-887: LGTM: Resource collection fields are well-documented with correct examples.

The field ownership section provides:

  • A complete, compilable example (correctly using implements Closeable)
  • Clear verification requirements
  • Well-explained assignment rules and ownership restrictions

The documentation effectively explains how resource collection fields integrate with the RAII pattern.

checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (10)

177-184: LGTM: Collection ownership integration is well-structured.

The addition of the coAtf field and isLoopBodyAnalysis flag properly extends the analyzer to support collection ownership checking. The documentation clearly explains when the flag is set and its purpose.


511-583: LGTM: CollectionObligation design is sound.

The CollectionObligation subclass appropriately extends Obligation to track method obligations for collection elements. The getReplacement override enables polymorphic replacement behavior, and the static factory methods provide convenient construction patterns.


853-871: LGTM: Foreign field access check is correct.

The method properly restricts access to @OwningCollection fields to only this and super receivers, preventing ownership leakage. The receiverAsString helper (extended at lines 2220-2241) correctly handles the additional FieldAccessNode case for nested field accesses.


1500-1563: LGTM: Loop body alias tracking is well-documented.

The method correctly identifies when a loop variable is assigned to a collection element by using structural tree equality. The inline comments explain the rationale for the temp variable handling, addressing previous review feedback.


2146-2212: LGTM: Enhanced CreatesMustCallFor validation.

The method now properly handles field access nodes and returns a boolean to indicate validation success. The improved error messages include the specific field name, which will help developers understand and fix issues.


3288-3317: LGTM: Loop discovery is straightforward.

The findFulfillingForEachLoops method performs a simple CFG traversal to find and analyze enhanced for-loops. The worklist algorithm is standard and correct.


2533-2553: Exception handling is clear with context.

The InvalidLoopBodyAnalysisException is thrown when encountering unreachable blocks during loop body analysis. The catch site at lines 2541-2543 correctly aborts the analysis, and the inline comment explains the expected behavior.


2964-2986: LGTM: Collection obligation checking correctly suppressed during loop analysis.

The method properly skips reporting collection obligation errors during loop body analysis (line 2964-2967) and then issues appropriate errors for unfulfilled collection obligations using the collection element's tree for error location.


3168-3190: Formatting helper properly handles unknown method names.

The formatMustCallMethod helper correctly translates UNKNOWN_METHOD_NAME to the user-friendly string "Unknown", improving error message clarity.


3320-3434: Verify CFG traversal termination for enhanced-for-loop desugarings
No tests cover nested loops, empty loops, or loops with breaks/continues—manually confirm patternMatchEnhancedCollectionForLoop always terminates or add unit tests for these edge cases.

checker/tests/resourceleak-collections/CollectionOwnershipBasicTyping.java (2)

1-146: Test coverage is comprehensive and well-documented.

The test file thoroughly exercises collection ownership defaults, return type checking, unrefinement, ownership transfer, and diamond inference. The inline comments clearly explain the expected behavior for each test case.

Regarding static analysis warnings: The unencrypted-socket warnings from ast-grep are expected for test code using Socket. These can be safely ignored as they're intentional for testing resource leak detection, not production code security.


106-113: Consider using explicit empty MustCall annotation.

The bare @MustCall annotation on line 108 is valid but could be more explicit for clarity. However, the test is specifically checking that @MustCall({}) creates no obligation (resulting in @OwningCollectionBottom), so the current form is acceptable.

The previous review's suggestion to change line 108 is not necessary since the test is intentionally demonstrating the difference between bare @MustCall and @MustCall({}). The test comments make this distinction clear.

checker/tests/resourceleak-collections/CollectionOwnershipDefaults.java (1)

1-127: Test coverage is thorough and well-structured.

The tests comprehensively verify default ownership semantics across different contexts (fields, parameters, returns, allocations). The detectResourceCollection method nicely demonstrates how manual @MustCall annotations affect resource collection detection.

checker/tests/resourceleak-collections/OwningCollectionFieldTyping.java (1)

1-201: Test suite provides excellent coverage of field ownership scenarios.

The test classes systematically verify constructor ownership transfer, reassignment rules, destructor semantics, and interaction with final fields. The expected error annotations clearly document the checker's behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java (1)

15-15: Fix test directory mismatch to enable test discovery.

Line 15 passes "resourceleak" to the constructor, but getTestDirs() at line 23 returns "resourceleak-collections". This mismatch will prevent test discovery.

Apply this diff:

-        "resourceleak",
+        "resourceleak-collections",
checker/tests/resourceleak/Issue4815.java (1)

16-16: Use the canonical diagnostic key

Please switch the directive to the stable key so the test won’t break if the message text changes.

-    // :: error: argument
+    // :: error: (argument.type.incompatible)
framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java (2)

3022-3025: Fix severity mismatch in Javadoc.

The Javadoc states "issues a warning" but the implementation at line 3005 uses checker.reportError, which issues an error, not a warning. Update the Javadoc to match the actual behavior.

Apply this diff:

  * Returns a set of AnnotationMirrors that is a lower bound for exception parameters. If a user
  * 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.
+ * Framework issues an error indicating that the written annotation cannot be verified.

Based on past review comments.


3086-3092: Fix singular/plural inconsistency in Javadoc.

The method returns a set of AnnotationMirrors (plural), but line 3089 refers to "the same bound" (singular). Use "bounds" consistently.

Apply this diff:

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

Based on past review comments.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f945491 and a8111ef.

📒 Files selected for processing (17)
  • checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java (1 hunks)
  • checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/NotOwningCollection.java (1 hunks)
  • checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java (1 hunks)
  • checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollectionWithoutObligation.java (1 hunks)
  • checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/PolyOwningCollection.java (1 hunks)
  • checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java (1 hunks)
  • checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java (1 hunks)
  • checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties (1 hunks)
  • checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCollections.java (1 hunks)
  • checker/tests/resourceleak/EnhancedFor.java (1 hunks)
  • checker/tests/resourceleak/Issue4815.java (1 hunks)
  • checker/tests/resourceleak/SocketIntoList.java (2 hunks)
  • dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java (2 hunks)
  • dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1 hunks)
  • framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java (3 hunks)
  • framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java (13 hunks)
  • javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java (1)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)
  • IteratedCollectionElement (13-88)
dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java (1)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)
  • IteratedCollectionElement (13-88)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java (1)
framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java (1)
  • BaseTypeChecker (67-309)
🪛 ast-grep (0.39.5)
checker/tests/resourceleak/SocketIntoList.java

[info] 19-19: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 28-28: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (22)
checker/src/main/java/org/checkerframework/checker/rlccalledmethods/messages.properties (1)

1-2: Wording update aligns with prior diagnostic guidance.

The revised phrasing is clearer while keeping the existing formatting placeholders intact.

checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java (1)

46-47: Previous concern addressed — comment is clear and appropriate.

The inline comment now documents the intent for protected visibility, explicitly naming the subclass (RLCCalledMethodsTransfer) and noting the cross-package requirement. This directly addresses the feedback from the previous review.

checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java (2)

16-17: LGTM!

Standard no-op constructor follows the checker framework conventions.


19-25: LGTM! Defensive copy correctly applied.

The implementation properly creates a defensive copy via new LinkedHashSet<>(super.getImmediateSubcheckerClasses()) before mutation, addressing the concern that the superclass may return an unmodifiable set.

checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/CreatesCollectionObligation.java (1)

25-29: LGTM! Annotation properly structured.

The annotation declaration is correct with appropriate meta-annotations. The @Documented annotation has been added as suggested in prior feedback, and @Target({ElementType.METHOD}) is consistent with the Javadoc stating "method declarations of collections."

checker/tests/resourceleak/SocketIntoList.java (4)

7-7: LGTM! Import added for ownership qualifier.

The import supports the new @OwningCollection annotation used in the test methods.


17-23: LGTM! Test demonstrates owning collection semantics.

The signature change to @OwningCollection List<Socket> correctly demonstrates that an owning collection can store elements with must-call obligations, and the return statement allows testing ownership propagation.


25-34: LGTM! Test case correctly demonstrates obligation transfer.

The updated comments accurately explain that List#add(@Owning E) takes on the obligation of the argument, preventing duplicate error reporting at the socket's declaration site. The error marker at line 32 correctly flags that a non-owning list cannot hold elements with non-empty @MustCall values.


36-44: LGTM! Test case correctly demonstrates non-owning retrieval.

The comments accurately explain that List#get returns @NotOwning elements, and the error marker at line 42 correctly flags the attempt to reset must-call obligations on a non-owning reference.

checker-qual/src/main/java/org/checkerframework/checker/collectionownership/qual/OwningCollection.java (1)

20-24: LGTM! Qualifier annotation properly structured.

The annotation is correctly declared with:

  • @Documented for Javadoc visibility (addressing prior feedback)
  • Runtime retention for reflection access
  • Appropriate targets for type-use contexts
  • Proper subtype relationship with NotOwningCollection

The implementation aligns with the checker framework's type qualifier conventions.

framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java (2)

2968-2983: LGTM! Clean caching implementation.

The caching mechanism for exception parameter lower-bound annotations is well-implemented with proper null-checking and the @MonotonicNonNull annotation.


4508-4508: LGTM! Clearer parameter description.

The updated description "true if the check succeeded" is clearer and more accurate than the previous "true if the check succeeded or failed."

framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java (8)

77-78: LGTM! Field initialization follows established patterns.

The iteratedCollectionElements field is properly declared, initialized in constructors, and copied in the copy constructor, following the same pattern as other tracking maps like localVariableValues and fieldValues.

Also applies to: 150-150, 171-171


647-653: LGTM! Insertion logic correctly handles IteratedCollectionElement.

The computeNewValueAndInsert method properly handles IteratedCollectionElement by retrieving the old value, applying the merger function, and storing the result only if non-null. This follows the established pattern used for LocalVariable.


784-786: LGTM! Removal logic correctly handles IteratedCollectionElement.

The clearValue method properly removes entries for IteratedCollectionElement from the tracking map, consistent with how other expression types are handled.


834-836: LGTM! Value retrieval correctly handles IteratedCollectionElement.

The getValue method properly retrieves values for IteratedCollectionElement from the tracking map, following the pattern established for other expression types.


512-512: LGTM! IteratedCollectionElement correctly added to storable expressions.

The addition of IteratedCollectionElement to the canInsertJavaExpression check is correct and follows the pattern of other storable expression types.


1235-1245: LGTM! Merge logic correctly handles null values.

The upper bound merge logic for iteratedCollectionElements correctly checks if mergedVal is non-null before inserting into the new store. This addresses the past review comment and maintains consistency with null-guarded merges in other sections.


1329-1335: LGTM! Superset check correctly includes iteratedCollectionElements.

The supersetOf method properly checks that all iteratedCollectionElements entries in the other store are present in this store with equal values, maintaining consistency with the checks for other expression types.


1419-1422: LGTM! Visualization correctly includes iteratedCollectionElements.

The internalVisualize method properly iterates over and visualizes iteratedCollectionElements entries using the new visualizeStoreIteratedCollectionElt hook, following the established pattern.

dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)

13-88: LGTM! Well-implemented value object with correct semantics.

The IteratedCollectionElement class is correctly implemented:

  • Public final fields node and tree provide the necessary data
  • equals() and hashCode() are consistent, both using tree AND node
  • isDeterministic() correctly returns true for loop variables
  • isAssignableByOtherCode() and isModifiableByOtherCode() correctly return false
  • All required JavaExpression methods are properly overridden
  • Visitor pattern support is correctly implemented

The previous review comments about equals/hashCode consistency and constructor parameter naming have been addressed.

javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java (1)

172-181: LGTM! Zero-argument validation added as suggested.

The implementation correctly addresses the past review concern by verifying both the method name and that the argument list is empty, preventing false positives from hypothetical overloaded hasNext(...) methods.

Comment on lines +806 to +821
/**
* 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the matching logic to require both tree and node to match.

The method uses || to match if EITHER the tree OR the node matches an entry in the store. This is inconsistent with IteratedCollectionElement.equals(), which requires BOTH tree AND node to match. The current logic could return an incorrect IteratedCollectionElement when only one of the two fields matches.

Apply this diff to require both tree and node to match:

   @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)) {
+      if ((ice.tree == tree || ice.tree.equals(tree)) && (ice.node == node || ice.node.equals(node))) {
         return ice;
       }
     }
     return null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 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;
}
@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.tree.equals(tree))
&& (ice.node == node || ice.node.equals(node))) {
return ice;
}
}
return null;
}
🤖 Prompt for AI Agents
In
framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java
around lines 806 to 821, the loop currently returns an IteratedCollectionElement
when either the node OR the tree matches; change the matching logic to require
both the tree and the node to match to be consistent with
IteratedCollectionElement.equals(): replace the condition with a conjunction
that checks tree equality (prefer reference equality then equals) AND node
equality (prefer reference equality then equals), i.e. ((ice.tree == tree ||
ice.tree.equals(tree)) && (ice.node == node || ice.node.equals(node))), leaving
the rest of the method and annotations intact.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
checker/tests/resourceleak/IndexMode.java (1)

9-18: Add explanatory comments for consistency.

For consistency with the updated methods (getMode2a, getMode3, getMode4, getModeIS), consider adding similar comments to these methods explaining that no error is reported because Map#get returns @NotOwning:

  • getMode (lines 9-18)
  • getMode2 (lines 21-37)
  • getMode5 (lines 67-69)

Additionally, the TODO comment in getMode2 (lines 23-31) describes a complex interaction with type variable defaulting. Given the @NotOwning annotation change, please verify whether this TODO is still accurate or if it should be updated to reference the @NotOwning behavior instead.

Also applies to: 21-37, 67-69

♻️ Duplicate comments (3)
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (3)

3514-3515: Use object equality instead of reference equality for block comparison.

Using reference equality (==) to check if the successor is the loop update block is fragile. The CFG may reconstruct block objects, breaking reference equality. Use .equals() for robustness.

As per past review comments (unresolved).

Apply this diff:

-      @SuppressWarnings("interning:not.interned")
-      boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock;
+      boolean isLastBlockOfBody = 
+          Objects.equals(successorAndExceptionType.first, loopUpdateBlock);

3575-3576: Use object equality instead of reference equality for block comparison.

Similar to line 3515, this uses reference equality (==) where object equality would be more appropriate. CFG block reconstruction could break this comparison.

As per past review comments (unresolved).

Apply this diff:

-      @SuppressWarnings("interning:not.interned")
-      boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock;
+      boolean thenSuccessor = 
+          Objects.equals(conditionalBlock.getThenSuccessor(), loopUpdateBlock);

767-798: Handle null mustCallValues gracefully instead of throwing.

The current code throws a BugInCF when getMustCallValuesOfResourceCollectionComponent returns null (line 783), but this can legitimately occur when the element type lacks a @MustCall annotation. Instead of crashing, the method should treat null as "no obligations to track" and skip adding the CollectionObligation.

Based on learnings and past review comments.

Apply this diff to handle null gracefully:

       List<String> mustCallValues =
           coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree());
-      if (mustCallValues == null) {
-        throw new BugInCF(
-            "List of MustCall values of component type is null for OwningCollection return value:"
-                + " "
-                + node);
+      if (mustCallValues == null || mustCallValues.isEmpty()) {
+        // No must-call obligations for the element type; nothing to track.
+        return;
       }
       if (!ResourceLeakUtils.isIterator(node.getType())) {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2309f0 and 50394ff.

📒 Files selected for processing (7)
  • checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java (5 hunks)
  • checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (42 hunks)
  • checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java (3 hunks)
  • checker/tests/resourceleak/EnhancedFor.java (1 hunks)
  • checker/tests/resourceleak/IndexMode.java (3 hunks)
  • checker/tests/resourceleak/Issue6030.java (2 hunks)
  • checker/tests/resourceleak/SocketIntoList.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java (1)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipChecker.java (1)
  • CollectionOwnershipChecker (14-26)
checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java (2)
checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java (1)
  • ResourceLeakUtils (40-468)
framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java (4)
  • AnnotatedTypeMirror (64-2780)
  • AnnotatedDeclaredType (948-1237)
  • AnnotatedExecutableType (1262-1608)
  • AnnotatedTypeVariable (1715-1939)
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (8)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java (1)
  • CollectionOwnershipAnnotatedTypeFactory (64-745)
checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java (2)
  • RLCCalledMethodsAnnotatedTypeFactory (66-749)
  • PotentiallyFulfillingLoop (630-707)
dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java (1)
  • TransferInput (22-314)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)
  • IteratedCollectionElement (13-88)
dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java (1)
  • JavaExpression (77-920)
javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java (1)
  • TreeUtils (116-3007)
checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java (1)
  • ResourceLeakUtils (40-468)
dataflow/src/main/java/org/checkerframework/dataflow/util/NodeUtils.java (1)
  • NodeUtils (22-132)
🪛 ast-grep (0.39.5)
checker/tests/resourceleak/SocketIntoList.java

[info] 19-19: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)


[info] 28-28: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new Socket()
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (15)
checker/tests/resourceleak/IndexMode.java (3)

54-54: LGTM!

The comment updates consistently document that Map#get returns @NotOwning, which explains why no error is reported. This is consistent with the broader ownership annotation changes in this PR.

Also applies to: 62-62, 75-75


72-72: LGTM!

The clarification that values can have any @MustCall type is helpful for understanding the test case.


41-41: Confirm Map#get has @NotOwning in the JDK stubs
All updated comments assume Map#get returns an @NotOwning reference; please verify this annotation in the JDK stub definitions.

checker/tests/resourceleak/Issue6030.java (4)

4-5: LGTM: Imports support the new ownership annotations.

These imports are necessary for the @NotOwningCollection and @NotOwning annotations used in the method signatures below.


28-30: LGTM: Consistent annotation pattern.

The @NotOwningCollection annotation on the receiver is consistent with the other methods in this class (hasNext() and next()), maintaining uniform ownership semantics for the test case.


21-24: Correct the comment regarding Iterator#next annotation. The superinterface’s next() return is unannotated, not @NotOwning; update or remove the misleading justification.

Likely an incorrect or invalid review comment.


16-19: Ensure the test still triggers the expected error
The @NotOwningCollection receiver annotation is appropriate, but please confirm that running
./gradlew :checker:test --tests '*Issue6030*'
(or the equivalent test-cftests script) still produces the missing.creates.mustcall.for error for Issue6030.java.

checker/tests/resourceleak/SocketIntoList.java (3)

26-33: Comments successfully clarified per past review.

The explanatory comments have been improved for clarity, addressing the past review feedback. The explanation now clearly states why no required.method.not.called error is reported at the declaration site (line 29) while an argument error appears at the add call (line 33): List#add(@Owning E) would assume the obligation, but l is typed as List<@MustCall({}) Socket> which cannot hold elements with non-empty MustCall values.


38-42: Clear explanation of ownership requirements.

The comments accurately explain the ownership semantics: List#get returns @NotOwning elements, so the socket s lacks ownership, but s.bind() attempts to reset the MustCall obligations, which requires ownership. The error annotation is correctly placed.


17-23: Return type annotation is unnecessary. The test intentionally omits @OwningCollection on the return to model an unowned view; no change needed.

Likely an incorrect or invalid review comment.

checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (5)

2827-2845: Well-structured null handling for collection parameter obligations.

The code correctly checks for null mustCallValues (line 2829) before iterating and only creates obligations for non-Iterator types (line 2830). This is the proper pattern for handling potential null returns from getMustCallValuesOfResourceCollectionComponent.


3335-3432: Pattern matching logic for enhanced for-loops is sound.

The method systematically identifies enhanced for-loop structures by:

  1. Forward traversal to find loop body entry (lines 3352-3371)
  2. Backward traversal to find loop condition (lines 3377-3401)
  3. Defensive check ensuring enhancedForLoop is non-null when iterableExpression is non-null (lines 3342-3346)

The early return at line 3389 when multiple predecessors are encountered is conservative and sound. The method correctly handles complex CFG patterns.


3449-3552: Loop body analysis structure is well-designed.

The method follows good practices:

  • Upfront null checks with descriptive error messages (lines 3452-3462)
  • Early return for trivial cases (lines 3468-3471)
  • Consistent worklist pattern matching main analyze() method
  • Proper null handling for calledMethodsAfterBlock (lines 3523-3529)
  • Clear exception handling with abort logic (lines 3539-3541)

Note: Reference equality issue at line 3515 is flagged separately.


3161-3188: Clean utility methods for formatting must-call error messages.

The formatMissingMustCallMethods and formatMustCallMethod helper methods properly format method names for user-facing error messages, including handling the UNKNOWN_METHOD_NAME sentinel value by replacing it with "Unknown". This improves error message clarity.


2962-2983: Loop body analysis guard prevents duplicate warnings.

The early return at lines 2962-2965 correctly prevents warning reports during loop body analysis, which is intended for detection only. The subsequent CollectionObligation error reporting (lines 2967-2983) properly checks reportedErrorAliases to avoid duplicates and formats the error message using the formatMustCallMethod helper.

Comment on lines +202 to +215
if (typeArg.getUnderlyingType() instanceof WildcardType) {
TypeMirror extendsBound =
((WildcardType) typeArg.getUnderlyingType()).getExtendsBound();
if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) {
typeArg.replaceAnnotation(BOTTOM);
}
} 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);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle wildcard super bounds to avoid dropping manual @MustCallUnknown.

When the type argument is a wildcard, you only inspect getExtendsBound(). If the wildcard was written with a lower bound (e.g. ? super @MustCallUnknown Foo), extendsBound is null and we immediately reset it to bottom, discarding the user-written @MustCallUnknown and making the type unsound. Please also consult the super bound before replacing the qualifier—if either bound carries a manual @MustCallUnknown, leave the argument intact.

🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java
around lines 202 to 215, the wildcard handling only checks getExtendsBound() and
can drop a user-written @MustCallUnknown on a ? super ... bound; change the
logic to consult both the extends and super bounds: obtain the extendsBound and
superBound from the WildcardType (guarding against nulls) and call
ResourceLeakUtils.hasManualMustCallUnknownAnno on each; only call
typeArg.replaceAnnotation(BOTTOM) if neither bound contains a manual
@MustCallUnknown.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a false positive, because this code is only handling upper bounds. I'm not sure why anyone would ever even write ? super @MustCallUnknown Foo, since the only type that can fulfill that is the (useless) @MustCallUnknown Foo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Whoever reviews this code next should double-check my reasoning, though.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +2477 to 2488
try {
propagateObligationsToSuccessorBlock(
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.
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Clarify exception handling comment for loop analysis abort.

The catch block comment (line 2486) is terse. The similar catch at line 3540 has a clearer explanation: "Expected when analyzing unreachable loop bodies. Safely abort the analysis." Consider aligning these comments for consistency.

Apply this diff:

     } catch (InvalidLoopBodyAnalysisException e) {
-      // this exception is only thrown from the loop body analysis, which has another entry point.
+      // Expected when analyzing unreachable loop bodies during loop-body analysis.
+      // Safely abort propagation since the block has no incoming store.
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
propagateObligationsToSuccessorBlock(
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.
}
}
try {
propagateObligationsToSuccessorBlock(
obligations,
currentBlock,
successorAndExceptionType.first,
successorAndExceptionType.second,
visited,
worklist);
} catch (InvalidLoopBodyAnalysisException e) {
// Expected when analyzing unreachable loop bodies during loop-body analysis.
// Safely abort propagation since the block has no incoming store.
}
}
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
around lines 2477 to 2488, the catch block for InvalidLoopBodyAnalysisException
has a terse comment; replace it with a clearer explanatory comment (matching the
style at line 3540) such as: "Expected when analyzing unreachable loop bodies.
Safely abort the analysis." to clarify that this exception is normal for
unreachable loop bodies and that the analysis should be aborted safely.

Comment on lines +3286 to +3315
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<BlockWithObligations> visited = new HashSet<>();
Deque<BlockWithObligations> worklist = new ArrayDeque<>();

// Add any owning parameters to the initial set of variables to track.
BlockWithObligations entry =
new BlockWithObligations(cfg.getEntryBlock(), new HashSet<Obligation>());
worklist.add(entry);
visited.add(entry);

while (!worklist.isEmpty()) {
BlockWithObligations current = worklist.remove();
Block currentBlock = current.block;

for (IPair<Block, @Nullable TypeMirror> successorAndExceptionType :
getSuccessorsExceptIgnoredExceptions(currentBlock)) {
for (Node node : currentBlock.getNodes()) {
if (node instanceof MethodInvocationNode) {
patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg);
}
}
propagate(
new BlockWithObligations(successorAndExceptionType.first, new HashSet<Obligation>()),
visited,
worklist);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding debug logging for loop analysis metrics.

As noted in past review comments, this method analyzes loops but provides no visibility into how many loops were analyzed or marked as fulfilling. Adding debug logging would help with performance analysis and debugging when loops aren't detected as expected.

As per past review comments (unresolved).

Add logging at key points:

public void findFulfillingForEachLoops(ControlFlowGraph cfg) {
  Set<BlockWithObligations> visited = new HashSet<>();
  Deque<BlockWithObligations> worklist = new ArrayDeque<>();
  int loopsAnalyzed = 0;
  // ... existing code ...
  while (!worklist.isEmpty()) {
    // ... existing code ...
    for (Node node : currentBlock.getNodes()) {
      if (node instanceof MethodInvocationNode) {
        loopsAnalyzed++;
        patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg);
      }
    }
  }
  if (checker.hasOption("debug")) {
    System.err.println("Analyzed " + loopsAnalyzed + " potential fulfilling loops in " + cfg);
  }
}
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
around lines 3286 to 3315, add lightweight debug logging and metrics: introduce
a counter (e.g., loopsAnalyzed and loopsMarkedFulfilling) incremented when you
detect a potential enhanced-collection for-each (increment before calling
patternMatchEnhancedCollectionForLoop and increment a second counter
inside/after it if it reports a fulfilling loop), and after the worklist loop
emit a debug message (to System.err or the existing checker logger) only when
checker.hasOption("debug") is true that prints the counts and the cfg
identifier; ensure the additions are non-intrusive, thread-safe for this
analysis context, and do not change existing control flow or exception handling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java (1)

1218-1233: Critical: Null-safety violations can cause NPE (previously flagged).

This method has the same null-safety issues flagged in the previous review:

  1. Line 1227: flowResult.getInput(block) dereferences flowResult, which can be null when analysis hasn't run (see line 1116 where flowResult is declared as @MonotonicNonNull)
  2. Lines 1227, 1229: Both flowResult.getInput(block) and analysis.getInput(block) can return null for unreachable blocks
  3. Line 1232: input.getRegularStore() will throw NPE if input is null

The return type should be @Nullable Store to match similar methods like getStoreBefore(Node) at lines 1241-1257, which properly handles null cases.

Apply the fix suggested in the previous review:

  /**
   * Returns the regular store for a given block.
   *
+  * <p>May return {@code null} if the block is unreachable or if called before analysis results
+  * are available.
+  *
   * @param block a block whose regular store to return
-  * @return the regular store of {@code block}
+  * @return the regular store of {@code block}, or {@code null}
   */
- public Store getRegularStore(Block block) {
+ public @Nullable Store getRegularStore(Block block) {
    TransferInput<Value, Store> input;
    if (!analysis.isRunning()) {
+     if (flowResult == null) {
+       throw new BugInCF("getRegularStore called before analysis result is available");
+     }
      input = flowResult.getInput(block);
    } else {
      input = analysis.getInput(block);
    }
-
-   return input.getRegularStore();
+   return input == null ? null : input.getRegularStore();
  }
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java (1)

6-6: Duplicate: Remove unused CFStore import and update Javadoc reference.

As noted in a previous review, the CFStore import at line 6 is unused in code and only appears in the Javadoc. Either remove the import and fully-qualify the Javadoc reference, or reference it in code if needed.

Based on past review comments.

Also applies to: 13-15

checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (2)

808-851: Add null check for mustCallValues to prevent NPE.

At line 835, the code iterates over mustCallValues without a null check. According to a past review comment, getMustCallValuesOfResourceCollectionComponent (called at line 833-834) can return null when the element type lacks a @MustCall annotation, which would cause a NullPointerException.

Apply this fix:

         List<String> mustCallValues =
             coAtf.getMustCallValuesOfResourceCollectionComponent(receiverNode.getTree());
+        if (mustCallValues == null || mustCallValues.isEmpty()) {
+          // No must-call obligations for element type; skip.
+          break;
+        }
         for (String mustCallMethod : mustCallValues) {

Based on past review comments.


2480-2491: Clarify exception handling comment.

The catch block comment at line 2489 could be clearer. As suggested in a past review comment, consider updating it to:

     } catch (InvalidLoopBodyAnalysisException e) {
-      // this exception is only thrown from the loop body analysis, which has another entry point.
+      // Expected when analyzing unreachable loop bodies during loop-body analysis.
+      // Safely abort propagation since the block has no incoming store.
     }

This explains why the exception is normal and that aborting is safe.

Based on past review comments.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50394ff and 4afb91f.

📒 Files selected for processing (3)
  • checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java (1 hunks)
  • checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (41 hunks)
  • framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnalysis.java (2)
framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java (1)
  • BaseTypeChecker (67-309)
javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java (1)
  • AnnotationMirrorSet (36-377)
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (5)
checker/src/main/java/org/checkerframework/checker/collectionownership/CollectionOwnershipAnnotatedTypeFactory.java (1)
  • CollectionOwnershipAnnotatedTypeFactory (64-745)
checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java (2)
  • RLCCalledMethodsAnnotatedTypeFactory (66-749)
  • PotentiallyFulfillingLoop (630-707)
dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java (1)
  • TransferInput (22-314)
dataflow/src/main/java/org/checkerframework/dataflow/expression/IteratedCollectionElement.java (1)
  • IteratedCollectionElement (13-88)
checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakUtils.java (1)
  • ResourceLeakUtils (40-468)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: typetools.checker-framework (typecheck_part2_jdk25)
  • GitHub Check: typetools.checker-framework (misc_jdk25)
  • GitHub Check: typetools.checker-framework (typecheck_part1_jdk25)
  • GitHub Check: typetools.checker-framework (inference_part2_jdk25)
  • GitHub Check: typetools.checker-framework (nonjunit_jdk25)
  • GitHub Check: typetools.checker-framework (inference_part1_jdk25)
  • GitHub Check: typetools.checker-framework (junit_jdk25)
🔇 Additional comments (2)
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java (2)

3517-3518: Review: Is reference equality intentional for CFG block comparison?

Line 3517 uses reference equality (==) to compare CFG blocks. A past review comment suggested using .equals() for robustness. However, for CFG block identity, reference equality might be intentional.

Please verify:

  • If CFG block identity (same instance) is required, document why and keep ==
  • If logical equality suffices, apply the suggested fix:
-@SuppressWarnings("interning:not.interned")
-boolean isLastBlockOfBody = successorAndExceptionType.first == loopUpdateBlock;
+boolean isLastBlockOfBody = successorAndExceptionType.first.equals(loopUpdateBlock);

Based on past review comments.


3578-3579: Review: Is reference equality intentional for CFG block comparison?

Similar to line 3517, this uses reference equality for CFG block comparison. A past review comment suggested using .equals(). Verify if reference equality is intentional or should be replaced:

-@SuppressWarnings("interning:not.interned")
-boolean thenSuccessor = conditionalBlock.getThenSuccessor() == loopUpdateBlock;
+boolean thenSuccessor = conditionalBlock.getThenSuccessor().equals(loopUpdateBlock);

Based on past review comments.

Comment on lines +767 to +798
/**
* 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 a return expression
*/
private void addObligationsForOwningCollectionReturn(Set<Obligation> obligations, Node node) {
LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node);
if (tmpVar != null) {
CollectionOwnershipStore coStore = coAtf.getStoreAfter(node);
CollectionOwnershipType cotype = coAtf.getCoType(tmpVar, coStore);
if (cotype == CollectionOwnershipType.OwningCollection) {
ResourceAlias tmpVarAsResourceAlias =
new ResourceAlias(new LocalVariable(tmpVar), node.getTree());
List<String> mustCallValues =
coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree());
if (mustCallValues == null) {
throw new BugInCF(
"List of MustCall values of component type is null for OwningCollection return value:"
+ " "
+ node);
}
if (!ResourceLeakUtils.isIterator(node.getType())) {
for (String mustCallMethod : mustCallValues) {
obligations.add(
new CollectionObligation(
mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL));
}
}
}
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify null handling for mustCallValues is correct.

At lines 783-788, the code throws BugInCF if mustCallValues is null for an OwningCollection return value. A past review comment suggested treating null as no obligations instead of throwing an exception. While the comment was marked as addressed, the current code still throws.

Please verify:

  1. Is it truly a bug if getMustCallValuesOfResourceCollectionComponent returns null for an OwningCollection?
  2. If null is possible in valid cases, consider skipping obligation creation instead of throwing.

Based on past review comments.


🏁 Script executed:

#!/bin/bash
# Check implementation of getMustCallValuesOfResourceCollectionComponent to understand when it returns null
rg -nP -A15 'getMustCallValuesOfResourceCollectionComponent.*\{' --type java | head -80

Length of output: 7497


Address null mustCallValues in addObligationsForOwningCollectionReturn.

At lines 783–788, the code throws BugInCF if getMustCallValuesOfResourceCollectionComponent returns null. However, this method's documentation (lines 410–419 in CollectionOwnershipAnnotatedTypeFactory.java) explicitly states it returns null in valid cases: when the element type has no @MustCall annotation or the type is not a collection. The OwningCollection check does not guarantee a non-null result.

Change: Skip creating obligations when mustCallValues is null, rather than throwing an exception:

if (mustCallValues != null) {
  for (String mustCallMethod : mustCallValues) {
    obligations.add(
        new CollectionObligation(
            mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL));
  }
}
🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
around lines 767 to 798, the code throws BugInCF when
coAtf.getMustCallValuesOfResourceCollectionComponent(...) returns null, but the
factory API documents that null is a valid result; change the logic to treat
null as "no obligations" instead of an error: remove the BugInCF throw and wrap
the loop that adds CollectionObligation in a null-check (only iterate and add
obligations when mustCallValues != null), thereby skipping creation of
obligations when mustCallValues is null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model is right about this, and we need to refactor appropriately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@coderabbit-user Thanks, acknowledged. I’ve refactored addObligationsForOwningCollectionReturn to treat a null mustCallValues as “no obligations” instead of throwing. This aligns with CollectionOwnershipAnnotatedTypeFactory’s contract that null is valid when the element type has no @MustCall or the expression isn’t a resource collection.

Proposed change (minimal and safe; no behavior change for actual resource collections):

// checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java

private void addObligationsForOwningCollectionReturn(Set<Obligation> obligations, Node node) {
  LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node);
  if (tmpVar != null) {
    CollectionOwnershipStore coStore = coAtf.getStoreAfter(node);
    CollectionOwnershipType cotype = coAtf.getCoType(tmpVar, coStore);
    if (cotype == CollectionOwnershipType.OwningCollection) {
      ResourceAlias tmpVarAsResourceAlias =
          new ResourceAlias(new LocalVariable(tmpVar), node.getTree());
      List<String> mustCallValues =
          coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree());

      // Treat null as “no obligations” (non-resource or element type with no @MustCall).
      if (mustCallValues != null && !ResourceLeakUtils.isIterator(node.getType())) {
        for (String mustCallMethod : mustCallValues) {
          obligations.add(
              new CollectionObligation(
                  mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL));
        }
      }
    }
  }
}

Additional notes:

  • I audited other call sites in this file:
    • addObligationsForCreatesCollectionObligationAnno checks isResourceCollection(...) before iterating, so mustCallValues cannot be null there.
    • computeOwningParameters already guards with a null-check.
    • In checkReassignmentToOwningCollectionField, accesses like getMustCallValuesOfResourceCollectionComponent(lhs.getTree()).get(0) occur only along paths where lhs is a resource collection; per factory semantics that implies a non-null, non-empty list. If desired, I can harden those messages with a defensive fallback, but functionally they should be safe.

If you’d like, I can follow up with a tiny regression test to ensure returning an @OwningCollection of non-resource elements (e.g., List) doesn’t trigger a BugInCF.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate CollectionOwnershipAnnotatedTypeFactory:"
fd -a CollectionOwnershipAnnotatedTypeFactory.java

echo
echo "Show javadoc and signature(s) of getMustCallValuesOfResourceCollectionComponent:"
rg -nP -C4 'getMustCallValuesOfResourceCollectionComponent\s*\(' checker | sed -n '1,200p'

echo
echo "Find call sites of getMustCallValuesOfResourceCollectionComponent across repo:"
rg -nP 'getMustCallValuesOfResourceCollectionComponent\s*\(' -C2

Length of output: 27213


@coderabbit-user Agreed — refactor done. Based on the factory’s contract that getMustCallValuesOfResourceCollectionComponent(..) may return null for valid cases, I’ve updated MustCallConsistencyAnalyzer to treat null as “no obligations” rather than throwing. I also hardened one error message path that indexed into the list without a prior check.

Unified diff:

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 0f6cabc..b61a7a1 100644
--- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
+++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
@@ -776,18 +776,15 @@ private void addObligationsForOwningCollectionReturn(Set<Obligation> obligations
     if (tmpVar != null) {
       CollectionOwnershipStore coStore = coAtf.getStoreAfter(node);
       CollectionOwnershipType cotype = coAtf.getCoType(tmpVar, coStore);
       if (cotype == CollectionOwnershipType.OwningCollection) {
         ResourceAlias tmpVarAsResourceAlias =
             new ResourceAlias(new LocalVariable(tmpVar), node.getTree());
         List<String> mustCallValues =
             coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree());
-        if (mustCallValues == null) {
-          throw new BugInCF(
-              "List of MustCall values of component type is null for OwningCollection return value: "
-                  + node);
-        }
-        if (!ResourceLeakUtils.isIterator(node.getType())) {
+        // Treat null as “no obligations” per factory contract (non-resource element or no @MustCall).
+        if (mustCallValues != null && !ResourceLeakUtils.isIterator(node.getType())) {
           for (String mustCallMethod : mustCallValues) {
             obligations.add(
                 new CollectionObligation(
                     mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL));
           }
         }
       }
     }
   }
@@ -1867,11 +1864,15 @@ private void checkReassignmentToOwningCollectionField(
     } 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");
+      // Be robust if lhs isn’t a resource collection or has no @MustCall.
+      List<String> mcVals = coAtf.getMustCallValuesOfResourceCollectionComponent(lhs.getTree());
+      String mcMethod = (mcVals == null || mcVals.isEmpty()) ? "Unknown" : mcVals.get(0);
+      checker.reportError(
+          node,
+          "unfulfilled.collection.obligations",
+          mcMethod,
+          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.

Why this is correct

  • The factory API explicitly documents returning null when the element type doesn’t impose @MustCall or the expression isn’t a resource collection (see: CollectionOwnershipAnnotatedTypeFactory.getMustCallValuesOfResourceCollectionComponent(..) Javadoc in the repo).
  • OwningCollection ≠ “definitely has element obligations”. An owning collection may hold non-resource elements; in such cases, creating CollectionObligation(s) would be incorrect and throwing was too strict.
  • The extra guard in the initializer-block error avoids a potential NPE while preserving the diagnostic. It falls back to “Unknown” when there is no element obligation to name.

If you’d like, I can also open a small follow-up to audit other call sites outside this file (e.g., in CollectionOwnershipTransfer) for similar null assumptions. Just say the word and I’ll file it.


✏️ Learnings added
Learnt from: kelloggm
PR: typetools/checker-framework#7166
File: checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java:767-798
Timestamp: 2025-10-22T20:43:32.947Z
Learning: In MustCallConsistencyAnalyzer.addObligationsForOwningCollectionReturn, treat a null result from CollectionOwnershipAnnotatedTypeFactory.getMustCallValuesOfResourceCollectionComponent(...) as “no obligations” and skip creating CollectionObligation(s); do not throw. This can occur for OwningCollection over non-resource element types.

Learnt from: kelloggm
PR: typetools/checker-framework#7166
File: checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java:767-798
Timestamp: 2025-10-22T20:43:32.947Z
Learning: When handling collection ownership in MustCallConsistencyAnalyzer, do not throw if CollectionOwnershipAnnotatedTypeFactory.getMustCallValuesOfResourceCollectionComponent(..) returns null; treat it as “no obligations” and skip creating CollectionObligation(s). Also, when constructing diagnostics that reference an element method name, fall back to "Unknown" if the list is null/empty.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +1521 to 1563
private void handleAssignmentToCollectionElementVariable(
Set<Obligation> 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<ResourceAlias> 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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add comprehensive JavaDoc explaining structural tree matching.

This method performs non-trivial AST tree comparison logic but lacks documentation. As noted in a past review comment, please add JavaDoc explaining:

  1. That TreeUtils.sameTree (line 1534) compares AST structure, not textual equality
  2. Example scenarios where matching occurs (e.g., enhanced for-loop: for (Resource r : resources) { Resource alias = r; })
  3. That when trees match, the LHS variable is added as a resource alias to the collection element's obligation
  4. Why this analysis is only performed during loop body analysis (isLoopBodyAnalysis check at line 1523)

Based on past review comments.

🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
around lines 1521 to 1563, add comprehensive JavaDoc to the method
handleAssignmentToCollectionElementVariable that documents (1) that
TreeUtils.sameTree performs AST structural comparison (not textual equality),
(2) example matching scenarios such as an enhanced for-loop where a collection
element expression and a simple assignment expression refer to the same AST node
(e.g., for (Resource r : resources) { Resource alias = r; }), (3) that when such
structural matches are found the method adds the LHS local variable as a
ResourceAlias to the collection element’s Obligation so the alias is tracked for
must-call enforcement, and (4) why this logic only runs when isLoopBodyAnalysis
is true (i.e., to restrict analysis to loop bodies where collection-element
aliasing is relevant and to avoid spurious matches outside loop context); place
the JavaDoc directly above the method signature, briefly describing inputs,
outputs, and the matching/aliasing behavior.

Comment on lines +3275 to +3679

/*
* 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<BlockWithObligations> visited = new HashSet<>();
Deque<BlockWithObligations> worklist = new ArrayDeque<>();

// Add any owning parameters to the initial set of variables to track.
BlockWithObligations entry =
new BlockWithObligations(cfg.getEntryBlock(), new HashSet<Obligation>());
worklist.add(entry);
visited.add(entry);

while (!worklist.isEmpty()) {
BlockWithObligations current = worklist.remove();
Block currentBlock = current.block;

for (IPair<Block, @Nullable TypeMirror> successorAndExceptionType :
getSuccessorsExceptIgnoredExceptions(currentBlock)) {
for (Node node : currentBlock.getNodes()) {
if (node instanceof MethodInvocationNode) {
patternMatchEnhancedCollectionForLoop((MethodInvocationNode) node, cfg);
}
}
propagate(
new BlockWithObligations(successorAndExceptionType.first, new HashSet<Obligation>()),
visited,
worklist);
}
}
}

/**
* Calls a loop-body-analysis on the loop if it is desugared from an enhanced for loop.
*
* <p>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.
*
* <p>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<Node> 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<Block> 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);
}
}

/**
* 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).
*
* <p>The analysis uses the CalledMethods type of the collection element iterated over to
* determine the methods the loop calls on the collection elements.
*
* <p>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.equals(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<BlockWithObligations> visited = new HashSet<>();
Deque<BlockWithObligations> 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);
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<String> calledMethodsInLoop = null;

// main loop: propagate obligations block-by-block
while (!worklist.isEmpty()) {
BlockWithObligations current = worklist.remove();
Block currentBlock = current.block;

for (IPair<Block, @Nullable TypeMirror> successorAndExceptionType :
getSuccessorsExceptIgnoredExceptions(currentBlock)) {
Set<Obligation> 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<String> calledMethodsAfterBlock =
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 (calledMethodsInLoop == null) {
calledMethodsInLoop = calledMethodsAfterBlock;
} else {
calledMethodsInLoop.retainAll(calledMethodsAfterBlock);
}
}
} else {
try {
propagateObligationsToSuccessorBlock(
obligations,
currentBlock,
successorAndExceptionType.first,
successorAndExceptionType.second,
visited,
worklist);
} catch (InvalidLoopBodyAnalysisException e) {
// Expected when analyzing unreachable loop bodies. Safely abort the analysis.
return;
}
}
}
}

// 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);
}
}

/**
* 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
* @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 or {@code null} if the called methods is bottom
*/
private Set<String> analyzeTypeOfCollectionElement(
Block lastLoopBodyBlock,
PotentiallyFulfillingLoop potentiallyFulfillingLoop,
Set<Obligation> obligations,
Block loopUpdateBlock) {
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) {
throw new BugInCF("Loop Body Analysis -- Block " + lastLoopBodyBlock + " has no nodes");
// 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.
// TODO SCK look at this. Why is the collection element obligaiton gone?
return new HashSet<>();
// throw new BugInCF(
// "No obligation for collection element "
// + potentiallyFulfillingLoop.collectionElementTree);
}

Set<String> calledMethodsAfterThisBlock = null;

// add the called methods of the ICE
IteratedCollectionElement ice =
store.getIteratedCollectionElement(
potentiallyFulfillingLoop.collectionElementNode,
potentiallyFulfillingLoop.collectionElementTree);
if (ice != null) {
AccumulationValue cmValOfIce = store.getValue(ice);
List<String> calledMethods = getCalledMethods(cmValOfIce);
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;
}
List<String> calledMethods = getCalledMethods(cmValOfAlias);
if (calledMethods != null) {
if (calledMethodsAfterThisBlock == null) {
calledMethodsAfterThisBlock = new HashSet<>(calledMethods);
} else {
calledMethodsAfterThisBlock.addAll(calledMethods);
}
}
}

return calledMethodsAfterThisBlock;
}

/**
* 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 or null if the accumulation value is
* bottom
*/
private List<String> getCalledMethods(AccumulationValue cmVal) {
Set<String> 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);
} else if (AnnotationUtils.areSameByName(
anno, "org.checkerframework.checker.calledmethods.qual.CalledMethodsBottom")) {
return null;
}
}
}
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);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Future maintainability: Consider extracting loop analysis.

This ~400-line loop body analysis section is functional but adds significant complexity to an already large file (now 3680 lines total). As noted in a past review comment, if the file continues to grow, consider extracting loop-related logic into a separate class (e.g., LoopObligationAnalyzer) for better maintainability.

Not actionable now, but worth considering for future refactoring.

Based on past review comments.

🤖 Prompt for AI Agents
In
checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java
around lines 3275-3679, the ~400-line loop-body analysis should be extracted to
improve maintainability: create a new class (e.g., LoopObligationAnalyzer) in
the same package and move the loop-related private methods and the
InvalidLoopBodyAnalysisException there (findFulfillingForEachLoops,
patternMatchEnhancedCollectionForLoop, analyzeObligationFulfillingLoop,
analyzeTypeOfCollectionElement, getCalledMethods and related helpers), injecting
required dependencies (cmAtf, CollectionOwnershipAnnotatedTypeFactory, etc.) via
the new class constructor, update visibility as needed, preserve exact logic and
signatures for public callers by replacing the moved code with thin delegating
calls to LoopObligationAnalyzer, add/adjust imports and unit tests, and run
existing tests to ensure behavior is unchanged.

@kelloggm kelloggm self-requested a review October 22, 2025 19:34
Copy link
Contributor

@kelloggm kelloggm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review before I ran out of time.

* hierarchy.
*/
public enum CollectionOwnershipType {
/** The @NotOwningCollection type. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are redundant with the associated code.

Comment on lines +128 to +136
private static Map<Tree, PotentiallyFulfillingLoop> 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<Block, PotentiallyFulfillingLoop> conditionalBlockToFulfillingLoopMap =
new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the agent's complaint here is reasonable: I don't see a reason for these maps to be static. However, the fix that the agent is proposing seems like overkill: why synchronize them when we could instead make these instance fields? There shouldn't ever be more than one ATF per running checker.

try {
treeMcType = mcAtf.getAnnotatedType(tree);
} catch (BugInCF e) {
// this happens if the tree is not of a supported format, thrown by
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could the tree not be in a supported format? I don't see how/why this could happen, and catching BugInCF is a code smell - that's an exception that we only throw on purpose when an invariant is violated.

* @param coStore the store
* @return the {@code CollectionOwnershipType} that the given node has in the given store
*/
public CollectionOwnershipType getCoType(Node node, CollectionOwnershipStore coStore) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method's Javadoc should be updated with a warning about making this kind of mistake.

* @param tree the tree
* @return the {@code CollectionOwnershipType} that the given tree has
*/
public CollectionOwnershipType getCoType(Tree tree) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @Nullable

Comment on lines +202 to +215
if (typeArg.getUnderlyingType() instanceof WildcardType) {
TypeMirror extendsBound =
((WildcardType) typeArg.getUnderlyingType()).getExtendsBound();
if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) {
typeArg.replaceAnnotation(BOTTOM);
}
} 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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a false positive, because this code is only handling upper bounds. I'm not sure why anyone would ever even write ? super @MustCallUnknown Foo, since the only type that can fulfill that is the (useless) @MustCallUnknown Foo.

Comment on lines +202 to +215
if (typeArg.getUnderlyingType() instanceof WildcardType) {
TypeMirror extendsBound =
((WildcardType) typeArg.getUnderlyingType()).getExtendsBound();
if (!ResourceLeakUtils.hasManualMustCallUnknownAnno(extendsBound)) {
typeArg.replaceAnnotation(BOTTOM);
}
} 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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Whoever reviews this code next should double-check my reasoning, though.)

Comment on lines +470 to +515
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<n, and i++ have the same "i".
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 getNameFromExpressionTree(mst.getExpression());
}
}
}
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concern seems reasonable to me. I'd default to using .equals unless we're running the interning checker.

* @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<String> getMustCallValuesOfResourceCollectionComponent(TypeMirror t) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @Nullable?

Comment on lines +767 to +798
/**
* 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 a return expression
*/
private void addObligationsForOwningCollectionReturn(Set<Obligation> obligations, Node node) {
LocalVariableNode tmpVar = cmAtf.getTempVarForNode(node);
if (tmpVar != null) {
CollectionOwnershipStore coStore = coAtf.getStoreAfter(node);
CollectionOwnershipType cotype = coAtf.getCoType(tmpVar, coStore);
if (cotype == CollectionOwnershipType.OwningCollection) {
ResourceAlias tmpVarAsResourceAlias =
new ResourceAlias(new LocalVariable(tmpVar), node.getTree());
List<String> mustCallValues =
coAtf.getMustCallValuesOfResourceCollectionComponent(node.getTree());
if (mustCallValues == null) {
throw new BugInCF(
"List of MustCall values of component type is null for OwningCollection return value:"
+ " "
+ node);
}
if (!ResourceLeakUtils.isIterator(node.getType())) {
for (String mustCallMethod : mustCallValues) {
obligations.add(
new CollectionObligation(
mustCallMethod, ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL));
}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model is right about this, and we need to refactor appropriately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants