Skip to content

RLC extension for resource collection#7602

Open
iamsanjaymalakar wants to merge 464 commits intotypetools:masterfrom
iamsanjaymalakar:rlc-for-collections
Open

RLC extension for resource collection#7602
iamsanjaymalakar wants to merge 464 commits intotypetools:masterfrom
iamsanjaymalakar:rlc-for-collections

Conversation

@iamsanjaymalakar
Copy link
Copy Markdown
Member

@iamsanjaymalakar iamsanjaymalakar commented Mar 31, 2026

This is the new PR for #7166

Sascha Kehrli and others added 30 commits June 24, 2025 22:30
…CreatesMustCallFor anno on enclosing method
Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java (1)

111-122: ⚠️ Potential issue | 🟠 Major

Make both add overloads explicitly @OwningCollection receivers.

Line 111 and Line 121 currently leave insertion receiver ownership weaker/implicit, while other mutators (checkAdd, remove) are explicit. This creates an inconsistent ownership contract on the public insertion paths.

💡 Proposed fix
-  public V add(LinkedHashKeyedSet<K, V> this, V o, int conflictBehavior, int equalBehavior) {
+  public V add(`@OwningCollection` LinkedHashKeyedSet<K, V> this, V o, int conflictBehavior, int equalBehavior) {
     K key = keyer.getKeyFor(o);
     V old = theMap.get(key);
     if (old == null
         || (eq(o, old) ? checkAdd(equalBehavior, old) : checkAdd(conflictBehavior, old)))
       theMap.put(key, o);
     return old;
   }

-  public boolean add(`@Owning` V o) {
+  public boolean add(`@OwningCollection` LinkedHashKeyedSet<K, V> this, `@Owning` V o) {
     return add(o, THROW_EXCEPTION, IGNORE) == null;
   }
#!/bin/bash
set -euo pipefail

TARGET="annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java"

echo "== Mutator signatures in LinkedHashKeyedSet =="
nl -ba "$TARGET" | rg -n "public .* add\\(|public .* remove\\(|private .* checkAdd\\(" -C1

echo
echo "== KeyedSet interface method signatures (for override compatibility) =="
fd -p "KeyedSet.java" annotation-file-utilities/src/main/java \
  | while read -r f; do
      echo "-- $f"
      nl -ba "$f" | rg -n "add\\(|remove\\(" -C1
    done

Expected result: all insertion/removal mutators in LinkedHashKeyedSet consistently require @OwningCollection receivers, and signatures remain compatible with KeyedSet.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 093f6549-bc61-47fc-85ee-156eb7fbf153

📥 Commits

Reviewing files that changed from the base of the PR and between 91e9f76 and 2e03c50.

📒 Files selected for processing (1)
  • annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java

Copy link
Copy Markdown
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.

♻️ Duplicate comments (4)
annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java (1)

126-127: ⚠️ Potential issue | 🟠 Major

Declare an explicit receiver qualifier on add(@owning V o) for contract consistency.

Line 126 is the only public insertion mutator without an explicit receiver qualifier. Please make the receiver explicit to keep ownership contracts consistent across mutators.

💡 Proposed fix
-  public boolean add(`@Owning` V o) {
+  public boolean add(`@OwningCollection` LinkedHashKeyedSet<K, V> this, `@Owning` V o) {
     return add(o, THROW_EXCEPTION, IGNORE) == null;
   }
#!/bin/bash
# Verify receiver-qualifier consistency for mutators in LinkedHashKeyedSet.
FILE="annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java"
rg -nP 'public\s+(boolean|V)\s+add\s*\(|public\s+boolean\s+remove\s*\(' "$FILE" -A2 -B1
# Expected: each public mutator signature shows an explicit receiver qualifier.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java`
around lines 126 - 127, The public mutator add(`@Owning` V o) is missing an
explicit receiver qualifier, breaking ownership-contract consistency with other
mutators; update the method signature for add to include the same explicit
receiver qualifier used on other mutators (e.g. the qualifier pattern applied to
remove/other insertion methods) so its ownership contract is explicit and
consistent with LinkedHashKeyedSet's other public mutators (refer to the
add(`@Owning` V o) method and the signatures of public remove/... methods to match
the qualifier).
checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java (3)

1375-1379: ⚠️ Potential issue | 🟠 Major

Compare the loop-variable Name with .equals(...).

On Line 1379, VariableTree.getName() is a Name; == relies on interning and can miss the assignment that binds the enhanced-for variable, so valid loops never resolve.

Suggested fix
-          isAssignmentOfLoopVariable =
-              iteratorVariableDeclaration.getName() == loopVariable.getName();
+          isAssignmentOfLoopVariable =
+              iteratorVariableDeclaration.getName().equals(loopVariable.getName());
Does javax.lang.model.element.Name guarantee reference equality for equal names, or should compiler code use .equals(...) / contentEquals(...) instead of ==?

Based on learnings: prefer using Name.equals(...) or Objects.equals(...) for com.sun.source.util.Name comparisons instead of == / !=.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java`
around lines 1375 - 1379, The comparison of loop variable Names in
RLCCalledMethodsAnnotatedTypeFactory uses reference equality
(VariableTree.getName() == loopVariable.getName()) which can fail; change the
check in the block handling AssignmentNode/VariableTree so that
isAssignmentOfLoopVariable compares Names by value (e.g., call .equals(...) or
use Objects.equals(...)) when comparing iteratorVariableDeclaration.getName()
and loopVariable.getName() to reliably detect the enhanced-for binding; leave
loopVariableNode and other logic unchanged.

1518-1534: ⚠️ Potential issue | 🟠 Major

Pick the latch, not the header, as loopUpdateBlock.

On Line 1533, backEdge.targetBlock is the loop header/condition block, not the block after one full iteration. Using it makes later store reads observe pre-iteration state and can miss methods called in the body. Record backEdge.sourceBlock instead.

Suggested fix
-          bestLoopUpdateBlock = backEdge.targetBlock;
+          bestLoopUpdateBlock = backEdge.sourceBlock;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java`
around lines 1518 - 1534, The loop selection stores the wrong block as the
update point: in the loop scan inside RLCCalledMethodsAnnotatedTypeFactory you
set bestLoopUpdateBlock to backEdge.targetBlock (the loop header/condition)
which makes later reads see pre-iteration state; change the assignment to use
backEdge.sourceBlock (the latch/block that flows back into the header) so
bestLoopUpdateBlock (and ultimately loopUpdateBlock) points to the
post-iteration update location; update any related comments to reflect that
WhileLoopResolutionCache.BlockEdge.sourceBlock is the intended latch.

383-397: ⚠️ Potential issue | 🟠 Major

Lambda-body collection loops are still dropped before lambda analysis.

Lines 517-535 deliberately defer lambda CFGs to later iterations, but the loop-state map is still keyed only by MethodTree. getMethodCollectionLoopState(...) / removeMethodCollectionLoopState(...) return null for UnderlyingAST.Kind.LAMBDA, and analyzeResolvedPotentiallyFulfillingCollectionLoops(...) only processes loops whose trees are in the current CFG. The enclosing-method pass therefore skips lambda-body loops and Line 1203 clears their state before the lambda CFG reaches postAnalyze(...). Use a method/lambda-aware key throughout the record/get/remove path.

Also applies to: 405-471, 517-535, 559-624, 1187-1203, 1227-1247

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java`:
- Around line 126-127: The public mutator add(`@Owning` V o) is missing an
explicit receiver qualifier, breaking ownership-contract consistency with other
mutators; update the method signature for add to include the same explicit
receiver qualifier used on other mutators (e.g. the qualifier pattern applied to
remove/other insertion methods) so its ownership contract is explicit and
consistent with LinkedHashKeyedSet's other public mutators (refer to the
add(`@Owning` V o) method and the signatures of public remove/... methods to match
the qualifier).

In
`@checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java`:
- Around line 1375-1379: The comparison of loop variable Names in
RLCCalledMethodsAnnotatedTypeFactory uses reference equality
(VariableTree.getName() == loopVariable.getName()) which can fail; change the
check in the block handling AssignmentNode/VariableTree so that
isAssignmentOfLoopVariable compares Names by value (e.g., call .equals(...) or
use Objects.equals(...)) when comparing iteratorVariableDeclaration.getName()
and loopVariable.getName() to reliably detect the enhanced-for binding; leave
loopVariableNode and other logic unchanged.
- Around line 1518-1534: The loop selection stores the wrong block as the update
point: in the loop scan inside RLCCalledMethodsAnnotatedTypeFactory you set
bestLoopUpdateBlock to backEdge.targetBlock (the loop header/condition) which
makes later reads see pre-iteration state; change the assignment to use
backEdge.sourceBlock (the latch/block that flows back into the header) so
bestLoopUpdateBlock (and ultimately loopUpdateBlock) points to the
post-iteration update location; update any related comments to reflect that
WhileLoopResolutionCache.BlockEdge.sourceBlock is the intended latch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4857c8da-78d4-4245-a838-e0420cdd1ca1

📥 Commits

Reviewing files that changed from the base of the PR and between 2e03c50 and 2c2468a.

📒 Files selected for processing (2)
  • annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java
  • checker/src/main/java/org/checkerframework/checker/rlccalledmethods/RLCCalledMethodsAnnotatedTypeFactory.java

Copy link
Copy Markdown
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java`:
- Around line 119-120: The ownership contract is inconsistent: add(V) is
annotated `@Owning` but the other insertion APIs are not, allowing bypasses via
theMap.put; update the KeyedSet interface and LinkedHashKeyedSet implementation
so the parameter on add(V, int, int) and replace(V) is annotated `@Owning`
(matching add(V)), and adjust the method signatures in both KeyedSet and
LinkedHashKeyedSet (where add(V,int,int) and replace(V) call theMap.put(...)) to
enforce the same ownership contract; also add any necessary imports or
annotations to compile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fbe65c73-cab4-4b9e-b610-c380d134b382

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2468a and 34c5027.

📒 Files selected for processing (1)
  • annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java

Comment on lines +119 to 120
public boolean add(@Owning V o) {
return add(o, THROW_EXCEPTION, IGNORE) == null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify ownership annotations on all mutators that can insert/replace elements.
fd -t f "LinkedHashKeyedSet.java" annotation-file-utilities \
  --exec sed -n '100,170p' {}

# Verify interface declarations for add/replace contract context.
fd -t f "KeyedSet.java" annotation-file-utilities \
  --exec sed -n '1,120p' {}

# Inspect all add/replace declarations in the concrete class.
rg -n --type=java '\b(public\s+)?(boolean|V)\s+(add|replace)\s*\(' \
  annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java

Repository: typetools/checker-framework

Length of output: 7389


Ownership contract is inconsistent across insertion APIs.

Line 119 requires @Owning for add(V), but insertion still occurs through add(V, int, int) (Line 109) and replace(V) (Line 148) without ownership annotations. Both methods call theMap.put(), creating bypass paths that weaken ownership/RLC guarantees.

Update both KeyedSet interface and LinkedHashKeyedSet implementation:

  • Add @Owning to add(V, int, int) parameter
  • Add @Owning to replace(V) parameter
💡 Changes needed

In KeyedSet.java:

-  public abstract V add(V v, int conflictBehavior, int equalBehavior);
+  public abstract V add(`@Owning` V v, int conflictBehavior, int equalBehavior);
-  public abstract V replace(V v);
+  public abstract V replace(`@Owning` V v);

In LinkedHashKeyedSet.java:

-  public V add(V o, int conflictBehavior, int equalBehavior) {
+  public V add(`@Owning` V o, int conflictBehavior, int equalBehavior) {
-  public V replace(V v) {
+  public V replace(`@Owning` V v) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/util/coll/LinkedHashKeyedSet.java`
around lines 119 - 120, The ownership contract is inconsistent: add(V) is
annotated `@Owning` but the other insertion APIs are not, allowing bypasses via
theMap.put; update the KeyedSet interface and LinkedHashKeyedSet implementation
so the parameter on add(V, int, int) and replace(V) is annotated `@Owning`
(matching add(V)), and adjust the method signatures in both KeyedSet and
LinkedHashKeyedSet (where add(V,int,int) and replace(V) call theMap.put(...)) to
enforce the same ownership contract; also add any necessary imports or
annotations to compile.

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@mernst CI jobs are failing on [daikon_part3_jdk25) due to missing annotations. How do I resolve them (add annotations)?

@mernst
Copy link
Copy Markdown
Member

mernst commented Apr 5, 2026

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@mernst
Copy link
Copy Markdown
Member

mernst commented Apr 5, 2026

That's weird. I can do so, even in an incognito tab. Here is the text:

Continuous Integration failures in related projects (daikon, plume-lib, etc.)

Continuous integration type-checks external projects such as daikon and plume-lib libraries. If you change the Checker Framework, then you might need to change code being type-checked — for example, to fix a newly-revealed defect (bug) or to remove a warning suppression that is no longer necessary.

Suppose that Checker Framework pull request #2222 is from fork ORG (that is, GitHub organization ORG) and branch B. The pull request is to organization typetools, project checker-framework, and branch master.

  1. Create a fork of the relevant project (such as daikon or plume-util) in GitHub organization ORG.
  2. Create a branch, in ORG, with exactly the same name as B.
  3. Create a pull request for the new branch. In the description of that pull request, write "Merge with https://github.com/typetools/checker-framework/pull/2222".
  4. Edit the description of pull request #2222 to refer back to the newly-created pull request: "Merge with https://github.com/UPSTREAM_ORG/PROJECT/pull/NNN".

When CI runs for the new pull request, it will use your fork and branch of the Checker Framework. When CI runs for your Checker Framework pull request, it will typecheck the newly-created branch. When all the CI runs pass, merge all the branches in quick succession — that is, click the squash-and-merge buttons one after another.

Copy link
Copy Markdown
Contributor

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Couple of very initial comments. My rough review plan:

  1. Review CollectionOwnershipChecker and related code in the collectionownership package, starting with the transfer functions
  2. Review MustCallConsistencyAnalyzer changes
  3. Review any other stuff that wasn't covered by 1 or 2

* the receiver.
*
* <p>This annotation should only be used on method declarations of collections, as defined by the
* CollectionOwnershipChecker, that is, {@code java.lang.Iterable} and {@code java.util.Iterator}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the reference to java.util.Iterator here out of date? Or does our checker actually treats Iterators as potentially owning collections?

Comment on lines +36 to +37
* Transfer function for the collection ownership type system. Its primary purpose is to create
* temporary variables for expressions (which allow those expressions to have refined information in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this really the primary purpose? Doesn't this code contain the primary logic for ownership inference, and isn't that just as important as temporary variables?

@Override
public TransferResult<CFValue, CollectionOwnershipStore> visitAssignment(
AssignmentNode node, TransferInput<CFValue, CollectionOwnershipStore> in) {
TransferResult<CFValue, CollectionOwnershipStore> res = super.visitAssignment(node, in);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is this super.visitAssignment call that does the ownership transfer from rhs to lhs, correct? If so, add a comment to that effect

Comment on lines +94 to +97
replaceInStores(
res,
hasExplicitNotOwningCollectionDeclaration(node) ? lhsJE : rhsJE,
atypeFactory.NOTOWNINGCOLLECTION);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a bit subtle. I'd add a comment like, "Make the RHS @NotOwningCollection to reflect the ownership transfer, unless there is a @NotOwningCollection annotation, in which case there is no transfer and the lhs should be @NotOwningCollection"

@msridhar
Copy link
Copy Markdown
Contributor

msridhar commented Apr 8, 2026

This PR is still incomplete. Significant documentation and restructuring remain.

@iamsanjaymalakar is this still true? If not, update the PR description?

Also, looks like there are some conflicts with the latest master branch.

@msridhar
Copy link
Copy Markdown
Contributor

msridhar commented Apr 8, 2026

@iamsanjaymalakar I have a high-level question. The logic for detecting loops that satisfy collection obligation and using them seems spread across many different places: MustCallVisitor#detectCollectionObligationFulfillingWhileLoop, MustCallConsistencyAnalyzer#analyzeResolvedPotentiallyFulfillingCollectionLoop, RLCCalledMethodsAnnotatedTypeFactory#analyzeResolvedPotentiallyFulfillingCollectionLoops, CollectionOwnershipTransfer#updateStoreForVerifiedFulfillingCollectionLoop, possibly other places. I was expecting most of this logic to live inside the Collection Ownership Checker.

Can you explain why the code is split up in the way that it is? Could we possibly consolidate more of it into the Collection Ownership Checker?

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@iamsanjaymalakar I have a high-level question. The logic for detecting loops that satisfy collection obligation and using them seems spread across many different places: MustCallVisitor#detectCollectionObligationFulfillingWhileLoop, MustCallConsistencyAnalyzer#analyzeResolvedPotentiallyFulfillingCollectionLoop, RLCCalledMethodsAnnotatedTypeFactory#analyzeResolvedPotentiallyFulfillingCollectionLoops, CollectionOwnershipTransfer#updateStoreForVerifiedFulfillingCollectionLoop, possibly other places. I was expecting most of this logic to live inside the Collection Ownership Checker.

Can you explain why the code is split up in the way that it is? Could we possibly consolidate more of it into the Collection Ownership Checker?

The split is mostly by responsibility, not by checker ownership.

MustCallVisitor is where the initial match happens because that step is purely syntactic. It sees the original Java source form before CFG lowering, and recognizes loops that iterate over a collection (for, while, etc.). This stage only asks whether the code is a candidate traversal over a resource collection. It does not try to prove anything.

RLCCalledMethodsAnnotatedTypeFactory is the next stage because AST matching is not enough. We still need CFG facts to certify the traversal:

  • enhanced for is desugared, so we need CFG reconstruction to find the actual body/update structure
  • while loops need CFG resolution to choose the correct loop-update block
  • certification must run after called-methods analysis has produced the relevant stores

RLCCalledMethodsAnnotatedTypeFactory#analyzeResolvedPotentiallyFulfillingCollectionLoops method is doing orchestration, not proof: it knows which candidates belong to the current analysis root, which ones have already been resolved, and when called-methods analysis has completed so loop certification can run.

MustCallConsistencyAnalyzer is doing proof. The proof is not a collection-ownership proof; it is a resource-leak proof. That is why the verification lives in MCCA rather than in the Collection Ownership Checker.

CollectionOwnershipTransfer is only the consumer of that proof. Once a loop traversal has been verified, collection ownership uses that fact to change the collection from @OwningCollection to @OwningCollectionWithoutObligation.

I'm not sure about how can I move all of them into CollectionOwnershipChecker. Let me know if you have any suggestions. @msridhar

Copy link
Copy Markdown
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

Caution

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

⚠️ Outside diff range comments (1)
annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java (1)

1460-1465: 🧹 Nitpick | 🔵 Trivial

Annotate get(int) explicitly to match the new accessor contract.

peek() and pop() now spell out the receiver qualifier, but Line 1498 get(int) still exposes the same element-access path via defaults alone. For a checker-facing API, that inconsistency makes the ownership surface harder to audit and easier to regress.

♻️ Proposed fix
-  public E get(int index) {
+  public E get(`@NotOwningCollection` ImmutableStack<E> this, int index) {

Also applies to: 1472-1476

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java`
around lines 1460 - 1465, The get(int) accessor must be annotated the same as
peek() and pop(): add the explicit receiver qualifier "@NotOwningCollection
ImmutableStack<E> this" to the get(int) method signature(s) so the ownership
contract is spelled out (match the annotations on peek() and pop()), ensuring
the ImmutableStack<E> receiver is annotated for checker-facing consistency.
♻️ Duplicate comments (8)
javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java (1)

1728-1732: ⚠️ Potential issue | 🔴 Critical

Guard empty argument lists before indexing getArguments().get(0).

Line 1731 can throw IndexOutOfBoundsException for zero-arg get() calls, which can crash analysis paths.

🐛 Proposed fix
 public static `@Nullable` ExpressionTree getIdxForGetCall(Tree tree) {
-  if ((tree instanceof MethodInvocationTree)
-      && isNamedMethodCall("get", (MethodInvocationTree) tree)) {
-    return ((MethodInvocationTree) tree).getArguments().get(0);
+  if (tree instanceof MethodInvocationTree) {
+    MethodInvocationTree invocationTree = (MethodInvocationTree) tree;
+    if (isNamedMethodCall("get", invocationTree) && !invocationTree.getArguments().isEmpty()) {
+      return invocationTree.getArguments().get(0);
+    }
   }
   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java` around
lines 1728 - 1732, getIdxForGetCall can throw IndexOutOfBoundsException when a
zero-argument "get" call is encountered; update the method to check the argument
list size before calling getArguments().get(0) (e.g., test
((MethodInvocationTree) tree).getArguments().isEmpty() or size() > 0) and only
return the first argument when present; keep the existing
isNamedMethodCall("get", (MethodInvocationTree) tree) check and return null for
empty-arg cases to avoid the exception.
docs/manual/resource-leak-checker.tex (5)

825-826: ⚠️ Potential issue | 🟠 Major

Resolve the static-field rule conflict for @NotOwningCollection.

Lines 825-826 say any static resource-collection field is rejected, but Lines 888-893 say @NotOwningCollection fields behave like ordinary non-owning references. Those rules conflict unless static @NotOwningCollection fields are explicitly carved out as legal, or forbidden in both places.

Also applies to: 888-893

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 825 - 826, The
documentation currently contradicts itself about static resource-collection
fields: the paragraph around the rule for resource collection ownership rejects
any static resource-collection field, while the later section describing
`@NotOwningCollection` implies such fields behave like ordinary non-owning
references; fix by reconciling the policy and making it explicit — either allow
static fields when annotated `@NotOwningCollection` or forbid static fields
universally. Update the text around the general rule (the “static fields are not
supported” sentence) and the `@NotOwningCollection` section so they state the same
policy, e.g., “Static resource-collection fields are forbidden except when
annotated `@NotOwningCollection`, which makes them legal non-owning references” or
“Static resource-collection fields are forbidden, and `@NotOwningCollection` is
not permitted on static fields,” and ensure both places reference
`@NotOwningCollection` by name for clarity.

963-964: ⚠️ Potential issue | 🟡 Minor

Remove the rendered TODO before merge.

This placeholder will ship as unfinished manual text. Replace it with the intended table or drop the subsection for now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 963 - 964, The rendered
TODO inside the subsection \subsectionAndLabel{JDK Method
Signatures}{resource-leak-collections-jdk-methods} must be removed before merge;
either replace the placeholder "[[TODO: Add a table with a number of
collection/iterable methods and their collection ownership annotations]]" with
the actual table of JDK collection/iterable methods and their ownership
annotations, or delete the entire subsection header and its placeholder content
if the table is not ready, ensuring no TODO/unrendered placeholder remains in
the final document.

146-163: ⚠️ Potential issue | 🟠 Major

Document @CreatesCollectionObligation alongside the other collection annotations.

This overview still omits @CreatesCollectionObligation, so readers never learn how methods that add new per-element obligations to an existing collection are modeled or how that differs from @CollectionFieldDestructor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 146 - 163, Add
documentation for `@CreatesCollectionObligation` alongside the existing collection
annotations (OwningCollection, NotOwningCollection, CollectionFieldDestructor):
state that `@CreatesCollectionObligation` marks methods that create per-element
obligations for an existing resource collection, document its parameters (if
any) and intended targets, give an example and explicitly contrast its semantics
with CollectionFieldDestructor (which cleans up whole fields) and with
owning/not-owning defaults, and link to the relevant
Section~\ref{resource-leak-collections} and the elements subsection so readers
can find details on per-element obligation modeling.

661-662: ⚠️ Potential issue | 🟠 Major

Don’t describe @NotOwningCollection as completely non-mutating.

These paragraphs still say non-owning references cannot add, remove, or overwrite elements. That overstates the restriction: the checker permits mutations that insert non-owning/safe elements. Please document that exception so the manual matches the checker.

Also applies to: 728-729

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 661 - 662, Update the two
paragraphs describing non-owning references to avoid saying they are completely
non-mutating: state that references annotated with `@NotOwningCollection` (or
otherwise non-owning) are restricted from arbitrary mutation but may perform
mutations that only insert or replace elements that are themselves
non-owning/safe (e.g., elements annotated as non-owning or otherwise provably
safe); revise the wording to mention this permitted exception and mirror the
same clarification in the other occurrence of the paragraph to keep the manual
consistent.

775-812: ⚠️ Potential issue | 🟡 Minor

This section still under-documents the accepted loop shapes.

It only documents enhanced for and one indexed-for pattern. If iterator/while traversals are now certified too, spell out those forms and their checks here; otherwise narrow the wording so the manual matches the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 775 - 812, The docs
currently only describe enhanced for and one indexed-for pattern for satisfying
collection obligations but the implementation also accepts additional traversal
shapes (or conversely the docs overclaim); update docs in
resource-leak-checker.tex to either (a) enumerate the exact accepted loop shapes
and checks (include iterator/while traversals and their required properties: no
early termination, no writes to loop variable/collection, accesses via
iterator.next() or collection.get(i) as appropriate, and reliance on
CalledMethods/@EnsuresCalledMethods for per-element calls) or (b) narrow the
wording to only claim enhanced-for and the indexed-for pattern shown; reference
the same terminology used in the text (CalledMethods,
`@EnsuresCalledMethods`("m"), `@OwningCollection`,
`@OwningCollectionWithoutObligation`) so the manual matches the implementation.
framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java (2)

78-79: ⚠️ Potential issue | 🟠 Major

Invalidate iterated-element facts on mutations, too.

iteratedCollectionElements is now first-class store state, but the mutation paths still never evict it. Side-effecting calls and the removeConflicting(...) paths for field/array/local assignments can all change the iterator or backing collection state, so keeping these entries makes the store stale and can leak called-method facts across iterations.

Minimal soundness fix
@@
     if (hasSideEffect) {
+      iteratedCollectionElements.clear();
@@
   protected void removeConflicting(FieldAccess fieldAccess, `@Nullable` V val) {
+    iteratedCollectionElements.clear();
@@
   protected void removeConflicting(ArrayAccess arrayAccess, `@Nullable` V val) {
+    iteratedCollectionElements.clear();
@@
   protected void removeConflicting(LocalVariable var) {
+    iteratedCollectionElements.clear();

Also applies to: 632-638

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java`
around lines 78 - 79, The iteratedCollectionElements map must be cleared/updated
whenever the store is mutated so iterator-related facts don't leak; modify all
mutation/assignment paths (including removeConflicting(...), field/array/local
assignment handlers, and any side-effecting call paths) to remove or invalidate
entries in iteratedCollectionElements when the target or its backing
collection/element could change. Locate uses of iteratedCollectionElements and
add logic in CFAbstractStore mutation methods (and in removeConflicting(...)) to
evict affected IteratedCollectionElement keys (or clear the map on broad-unknown
mutations) so iterator facts are not preserved across mutations.

817-820: ⚠️ Potential issue | 🟡 Minor

Require both key components to match in getIteratedCollectionElement.

This || chain is looser than IteratedCollectionElement.equals(...), which keys entries on both tree and node. Two distinct keys that share only one component can therefore resolve to the wrong store entry, and the result depends on HashMap iteration order.

Suggested fix
   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)) {
+      boolean treeMatches = ice.tree == tree || ice.tree.equals(tree);
+      boolean nodeMatches = ice.node == node || ice.node.equals(node);
+      if (treeMatches && nodeMatches) {
         return ice;
       }
     }
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java`
around lines 817 - 820, getIteratedCollectionElement currently matches a key if
either the node or tree matches, which can return the wrong
IteratedCollectionElement; change the matching logic in
getIteratedCollectionElement to require both components match (i.e., tree AND
node) using null-safe equality (e.g., Objects.equals) against
iteratedCollectionElements' keys (IteratedCollectionElement ice), so replace the
|| chain with a conjunction that checks both ice.tree vs tree and ice.node vs
node (or delegating to IteratedCollectionElement.equals(...) if appropriate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/manual/resource-leak-checker.tex`:
- Around line 665-672: The definition of "resource collection" is inconsistent
with the example: update the rule so the type parameter upper bound can be
either a non-empty `@MustCall` type or `@MustCallUnknown` (i.e., treat
`@MustCallUnknown` as a valid must-call obligation for the purposes of the
checker), and then keep the example Iterable<? extends `@MustCallUnknown` Object>
as-is; reference the "resource collection" term and the `@MustCall` /
`@MustCallUnknown` type parameter upper bound in the sentence to make the intent
explicit.

In
`@framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java`:
- Around line 1199-1214: The method getRegularStore(Block) may dereference a
null TransferInput because flowResult.getInput(block) and
analysis.getInput(block) are `@Nullable`; change the method to return `@Nullable`
Store and defend against null by checking if input is null and returning null
(rather than calling input.getRegularStore()), keeping the existing behavior
when input is non-null; update the method signature and any Javadoc to reflect
`@Nullable` and reference getRegularStore(Block), flowResult.getInput,
analysis.getInput, and TransferInput<Value, Store> in your changes.

---

Outside diff comments:
In
`@annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java`:
- Around line 1460-1465: The get(int) accessor must be annotated the same as
peek() and pop(): add the explicit receiver qualifier "@NotOwningCollection
ImmutableStack<E> this" to the get(int) method signature(s) so the ownership
contract is spelled out (match the annotations on peek() and pop()), ensuring
the ImmutableStack<E> receiver is annotated for checker-facing consistency.

---

Duplicate comments:
In `@docs/manual/resource-leak-checker.tex`:
- Around line 825-826: The documentation currently contradicts itself about
static resource-collection fields: the paragraph around the rule for resource
collection ownership rejects any static resource-collection field, while the
later section describing `@NotOwningCollection` implies such fields behave like
ordinary non-owning references; fix by reconciling the policy and making it
explicit — either allow static fields when annotated `@NotOwningCollection` or
forbid static fields universally. Update the text around the general rule (the
“static fields are not supported” sentence) and the `@NotOwningCollection` section
so they state the same policy, e.g., “Static resource-collection fields are
forbidden except when annotated `@NotOwningCollection`, which makes them legal
non-owning references” or “Static resource-collection fields are forbidden, and
`@NotOwningCollection` is not permitted on static fields,” and ensure both places
reference `@NotOwningCollection` by name for clarity.
- Around line 963-964: The rendered TODO inside the subsection
\subsectionAndLabel{JDK Method
Signatures}{resource-leak-collections-jdk-methods} must be removed before merge;
either replace the placeholder "[[TODO: Add a table with a number of
collection/iterable methods and their collection ownership annotations]]" with
the actual table of JDK collection/iterable methods and their ownership
annotations, or delete the entire subsection header and its placeholder content
if the table is not ready, ensuring no TODO/unrendered placeholder remains in
the final document.
- Around line 146-163: Add documentation for `@CreatesCollectionObligation`
alongside the existing collection annotations (OwningCollection,
NotOwningCollection, CollectionFieldDestructor): state that
`@CreatesCollectionObligation` marks methods that create per-element obligations
for an existing resource collection, document its parameters (if any) and
intended targets, give an example and explicitly contrast its semantics with
CollectionFieldDestructor (which cleans up whole fields) and with
owning/not-owning defaults, and link to the relevant
Section~\ref{resource-leak-collections} and the elements subsection so readers
can find details on per-element obligation modeling.
- Around line 661-662: Update the two paragraphs describing non-owning
references to avoid saying they are completely non-mutating: state that
references annotated with `@NotOwningCollection` (or otherwise non-owning) are
restricted from arbitrary mutation but may perform mutations that only insert or
replace elements that are themselves non-owning/safe (e.g., elements annotated
as non-owning or otherwise provably safe); revise the wording to mention this
permitted exception and mirror the same clarification in the other occurrence of
the paragraph to keep the manual consistent.
- Around line 775-812: The docs currently only describe enhanced for and one
indexed-for pattern for satisfying collection obligations but the implementation
also accepts additional traversal shapes (or conversely the docs overclaim);
update docs in resource-leak-checker.tex to either (a) enumerate the exact
accepted loop shapes and checks (include iterator/while traversals and their
required properties: no early termination, no writes to loop
variable/collection, accesses via iterator.next() or collection.get(i) as
appropriate, and reliance on CalledMethods/@EnsuresCalledMethods for per-element
calls) or (b) narrow the wording to only claim enhanced-for and the indexed-for
pattern shown; reference the same terminology used in the text (CalledMethods,
`@EnsuresCalledMethods`("m"), `@OwningCollection`,
`@OwningCollectionWithoutObligation`) so the manual matches the implementation.

In
`@framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java`:
- Around line 78-79: The iteratedCollectionElements map must be cleared/updated
whenever the store is mutated so iterator-related facts don't leak; modify all
mutation/assignment paths (including removeConflicting(...), field/array/local
assignment handlers, and any side-effecting call paths) to remove or invalidate
entries in iteratedCollectionElements when the target or its backing
collection/element could change. Locate uses of iteratedCollectionElements and
add logic in CFAbstractStore mutation methods (and in removeConflicting(...)) to
evict affected IteratedCollectionElement keys (or clear the map on broad-unknown
mutations) so iterator facts are not preserved across mutations.
- Around line 817-820: getIteratedCollectionElement currently matches a key if
either the node or tree matches, which can return the wrong
IteratedCollectionElement; change the matching logic in
getIteratedCollectionElement to require both components match (i.e., tree AND
node) using null-safe equality (e.g., Objects.equals) against
iteratedCollectionElements' keys (IteratedCollectionElement ice), so replace the
|| chain with a conjunction that checks both ice.tree vs tree and ice.node vs
node (or delegating to IteratedCollectionElement.equals(...) if appropriate).

In `@javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java`:
- Around line 1728-1732: getIdxForGetCall can throw IndexOutOfBoundsException
when a zero-argument "get" call is encountered; update the method to check the
argument list size before calling getArguments().get(0) (e.g., test
((MethodInvocationTree) tree).getArguments().isEmpty() or size() > 0) and only
return the first argument when present; keep the existing
isNamedMethodCall("get", (MethodInvocationTree) tree) check and return null for
empty-arg cases to avoid the exception.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 106e3096-831b-4d3d-8be4-0c36f754abe3

📥 Commits

Reviewing files that changed from the base of the PR and between d2d1a36 and ae959a1.

📒 Files selected for processing (7)
  • annotation-file-utilities/src/main/java/org/checkerframework/afu/scenelib/io/ASTPath.java
  • dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java
  • docs/manual/resource-leak-checker.tex
  • framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java
  • framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java
  • framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java
  • javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java

Comment on lines +665 to +672
For the purposes of this checker, \textit{resource collections} are precisely defined, as the objects of interest for both the type system and obligation tracking. A Java expression is a \textit{resource collection} if it is:

\begin{enumerate}
\item A \texttt{java.util.Iterable} or implementation thereof (which includes the entire Java Collections framework), and:%, or a \texttt{java.util.Iterator}
\item Its type parameter upper bound has non-empty \texttt{MustCall} type.
\end{enumerate}

For example, expressions of type \texttt{List<Socket>} or \texttt{Iterable<? extends @MustCallUnknown Object>} are both resource collections, but one of type \texttt{Set<String>} is not.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align the formal definition with the example.

Line 669 says a resource collection must have an element upper bound with a non-empty @MustCall type, but Line 672 then lists Iterable<? extends @MustCallUnknown Object> as an example. Please either broaden the definition to cover unknown must-call obligations or replace the example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/manual/resource-leak-checker.tex` around lines 665 - 672, The definition
of "resource collection" is inconsistent with the example: update the rule so
the type parameter upper bound can be either a non-empty `@MustCall` type or
`@MustCallUnknown` (i.e., treat `@MustCallUnknown` as a valid must-call obligation
for the purposes of the checker), and then keep the example Iterable<? extends
`@MustCallUnknown` Object> as-is; reference the "resource collection" term and the
`@MustCall` / `@MustCallUnknown` type parameter upper bound in the sentence to make
the intent explicit.

Comment on lines +1199 to +1214
/**
* Returns the regular store for a given block.
*
* @param block a block whose regular store to return
* @return the regular store of {@code block}
*/
public Store getRegularStore(Block block) {
TransferInput<Value, Store> input;
if (!analysis.isRunning()) {
input = flowResult.getInput(block);
} else {
input = analysis.getInput(block);
}

return input.getRegularStore();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how similar store-retrieval methods handle null

# Look for getInput calls and null handling patterns
rg -n -A5 -B2 'getInput\(' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java

# Check if TransferInput getInput can return null based on its declaration
ast-grep --pattern 'TransferInput<$_, $_> getInput($_) { $$$ }'

Repository: typetools/checker-framework

Length of output: 335


🏁 Script executed:

# Find and examine the getStoreBefore method to compare
rg -n -A10 'getStoreBefore\(Tree' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java

Repository: typetools/checker-framework

Length of output: 1414


🏁 Script executed:

# Find the class definition of flowResult and analysis to understand their types
rg -n 'flowResult\s*=' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java | head -5

rg -n 'analysis\s*=' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java | head -5

# Also look for field declarations
rg -n -B2 'private.*flowResult\|protected.*flowResult\|public.*flowResult' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java

rg -n -B2 'private.*analysis\|protected.*analysis\|public.*analysis' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java

Repository: typetools/checker-framework

Length of output: 352


🏁 Script executed:

# Check for `@Nullable` annotations in the file to understand the pattern
rg -n '@Nullable' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java | head -20

Repository: typetools/checker-framework

Length of output: 1562


🏁 Script executed:

# Search for any usage of getInput with null checking
rg -n -B2 -A2 'getInput.*null\|null.*getInput' framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java

Repository: typetools/checker-framework

Length of output: 53


🏁 Script executed:

# Search for getInput method definitions in the codebase
fd -t f '\.java$' | xargs rg -l 'getInput\(' | head -10

Repository: typetools/checker-framework

Length of output: 881


🏁 Script executed:

# Look for TransferResult or FlowResult class definitions
fd -t f 'TransferResult.java|FlowResult.java|AnalysisResult.java' framework/

Repository: typetools/checker-framework

Length of output: 53


🏁 Script executed:

# Search for method signature of getInput in analysis and flowResult related classes
rg -n 'TransferInput.*getInput|AnalysisResult.*getInput' framework/ -t java

Repository: typetools/checker-framework

Length of output: 53


🏁 Script executed:

# Find the AnalysisResult class and look for getInput method
rg -n -A10 'public.*getInput\(Block' dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java

Repository: typetools/checker-framework

Length of output: 404


🏁 Script executed:

# Also search in Analysis class
rg -n -A10 'public.*getInput\(Block' dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java

Repository: typetools/checker-framework

Length of output: 53


🏁 Script executed:

# Check AbstractAnalysis as well
rg -n -A10 'getInput\(Block' dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java

Repository: typetools/checker-framework

Length of output: 53


🏁 Script executed:

# Look for the return type annotation on getInput methods
rg -n '@Nullable.*getInput\(|getInput.*@Nullable' dataflow/src/main/java/org/checkerframework/dataflow/analysis/

Repository: typetools/checker-framework

Length of output: 651


Potential null pointer dereference on input.getRegularStore().

Both flowResult.getInput(block) and analysis.getInput(block) can return null (confirmed by @Nullable annotation in their signatures), but there is no null check before calling input.getRegularStore(). This is inconsistent with similar methods in this class like getStoreBefore(Tree), getStoreAfter(Tree), getRegularExitStore(Tree), and getExceptionalExitStore(Tree), which all return @Nullable Store.

Consider either adding a null check and returning @Nullable Store, or documenting a precondition that the block must have a valid input. Given the pattern in this class, returning @Nullable Store is more consistent with related methods.

Suggested fix
-  public Store getRegularStore(Block block) {
+  public `@Nullable` Store getRegularStore(Block block) {
     TransferInput<Value, Store> input;
     if (!analysis.isRunning()) {
       input = flowResult.getInput(block);
     } else {
       input = analysis.getInput(block);
     }
 
+    if (input == null) {
+      return null;
+    }
     return input.getRegularStore();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java`
around lines 1199 - 1214, The method getRegularStore(Block) may dereference a
null TransferInput because flowResult.getInput(block) and
analysis.getInput(block) are `@Nullable`; change the method to return `@Nullable`
Store and defend against null by checking if input is null and returning null
(rather than calling input.getRegularStore()), keeping the existing behavior
when input is non-null; update the method signature and any Javadoc to reflect
`@Nullable` and reference getRegularStore(Block), flowResult.getInput,
analysis.getInput, and TransferInput<Value, Store> in your changes.

@msridhar
Copy link
Copy Markdown
Contributor

msridhar commented Apr 8, 2026

Thanks, @iamsanjaymalakar. To make sure we are on the same page, here's what I think needs to happen at a high level. We need to identify loops that iterate over a (resource) collection. When we identify a loop, I'm assuming we know all CFG nodes in the loop, the loop head, and the loop variable corresponding to an element of the collection. For each such loop, we need to check two things:

  1. For each loop back edge (from a node inside the loop to the loop head), at the source of the back edge, all @MustCall methods for the loop variable are in its inferred @CalledMethods set.
  2. There are no early exits in the loop, due to break, throwing an exception (modulo our list of ignored exception types), etc.

If a loop over a resource collection satisfies 1 and 2, it is a certified disposal loop.

Then, in inference of collection ownership types, if a reference to a collection c is @OwningCollection, and there is a certified disposal loop over c, then immediately after the loop (there should be only one relevant loop exit edge), c becomes @OwningCollectionWithoutObligation.

I think it's also useful to keep in mind the current sub-checker relationships for the RLC:

ResourceLeakChecker -> CollectionOwnershipChecker -> RLCCalledMethodsChecker -> MustCallChecker

Now to your comment:

MustCallVisitor is where the initial match happens because that step is purely syntactic. It sees the original Java source form before CFG lowering, and recognizes loops that iterate over a collection (for, while, etc.). This stage only asks whether the code is a candidate traversal over a resource collection. It does not try to prove anything.

All checkers can have a visitor that operates over ASTs. Why does this logic need to live in the MustCallVisitor?

RLCCalledMethodsAnnotatedTypeFactory is the next stage because AST matching is not enough. We still need CFG facts to certify the traversal:

  • enhanced for is desugared, so we need CFG reconstruction to find the actual body/update structure
  • while loops need CFG resolution to choose the correct loop-update block
  • certification must run after called-methods analysis has produced the relevant stores

Again, all checkers usually do inference and have access to CFGs (including the Must Call Checker). I don't understand why the bullets listed above need to happen in the RLC Called Methods Checker.

RLCCalledMethodsAnnotatedTypeFactory#analyzeResolvedPotentiallyFulfillingCollectionLoops method is doing orchestration, not proof: it knows which candidates belong to the current analysis root, which ones have already been resolved, and when called-methods analysis has completed so loop certification can run.

I don't understand what "orchestration" or "resolved" mean in this sentence.

MustCallConsistencyAnalyzer is doing proof. The proof is not a collection-ownership proof; it is a resource-leak proof. That is why the verification lives in MCCA rather than in the Collection Ownership Checker.

This one I don't understand either. Is the idea that the check being performed at back edges is a consistency check, so it's somehow nice to re-use the consistency analyzer logic for this? Do we handle cases like must-aliases of the loop variable then? I guess that idea makes sense, but I still don't know why it's getting called from the RLC Called Methods Checker.

CollectionOwnershipTransfer is only the consumer of that proof. Once a loop traversal has been verified, collection ownership uses that fact to change the collection from @OwningCollection to @OwningCollectionWithoutObligation.

This makes sense. I just don't understand why the orchestration is so complicated. Is it because the Collection Ownership Checker cannot easily access the types inferred by the sub-checkers?

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

iamsanjaymalakar commented Apr 9, 2026

When we identify a loop, I'm assuming we know all CFG nodes in the loop, the loop head, and the loop variable corresponding to an element of the collection.

Not at the initial match point. There we only know the source-level loop shape and the relevant trees. The full CFG structure is recovered later; for enhanced for and the supported while forms, things like the actual body/update blocks come from the CFG resolution step, not from the visitor.

All checkers can have a visitor that operates over ASTs. Why does this logic need to live in the MustCallVisitor?

There is no fundamental reason it has to be in MustCallVisitor; it could also be done in CollectionOwnershipVisitor. I kept it in MustCallVisitor mainly because that is where the initial implementation already did the source-level matching.

Again, all checkers usually do inference and have access to CFGs (including the Must Call Checker). I don't understand why the bullets listed above need to happen in the RLC Called Methods Checker.

This does not inherently have to live in RLCCalledMethodsAnnotatedTypeFactory. I kept it there because that was already the place in the existing structure that owned the per-method loop state in collectionLoopStateByEnclosingMethod and whose postAnalyze(...) runs after Called Methods analysis has produced the stores that certification needs. So the choice was mostly about reusing that lifecycle, not about the other checkers lacking CFG access. Let me know if you have any suggestions on this.

I don't understand what "orchestration" or "resolved" mean in this sentence.

The AST match gives us a candidate loop, but not all of the CFG information that MCCA needs to certify it. After the missing CFG pieces are filled in, the loop is in a resolved state. By “orchestration”, I mean that RLCCalledMethodsAnnotatedTypeFactory is doing the bookkeeping around that process: it tracks the candidates, resolves the missing CFG pieces, and then calls MCCA on the resolved loops.

This one I don't understand either. Is the idea that the check being performed at back edges is a consistency check, so it's somehow nice to re-use the consistency analyzer logic for this? Do we handle cases like must-aliases of the loop variable then? I guess that idea makes sense, but I still don't know why it's getting called from the RLC Called Methods Checker.

Yes, the reason it is in MCCA is basically that this is already using the same consistency machinery: it seeds an obligation for the iterated element, propagates obligations through the loop body, updates aliases through assignments, and then checks the @CalledMethods information for the element and its aliases at the relevant back edges.

This makes sense. I just don't understand why the orchestration is so complicated. Is it because the Collection Ownership Checker cannot easily access the types inferred by the sub-checkers?

I agree with you on this. I think we'll need a seprate place/class that does all the orchestration. The matched/resolved loop state is tracked on the RLCC Called Methods side, and the certification needs to run in a postAnalyze(...) phase after Called Methods analysis has finished, so that is where the orchestration ended up.

@msridhar

@msridhar
Copy link
Copy Markdown
Contributor

msridhar commented Apr 9, 2026

Quick question, why do we need to do AST matching to find source-level loop shapes? Why can't everything be done at the CFG level?

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

Quick question, why do we need to do AST matching to find source-level loop shapes? Why can't everything be done at the CFG level?

@msridhar I'm not too sure about how hard that would be. I extended following the initial implementation (indexed-for in AST, enhanced for via CFG) to while loops. Matching in the AST stage seemed natuaral to find the loop condition and the collection being iterated (just to identify the potential loops). I don't know how complicated it would be to do so from CFG.

Copy link
Copy Markdown
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: 1

♻️ Duplicate comments (1)
checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java (1)

261-266: ⚠️ Potential issue | 🔴 Critical

Skip the current checker when probing for an enclosing RLC hierarchy.

Starting the scan at checker still makes a standalone MustCallChecker look like it is inside the RLC hierarchy, so this helper can call the RLCC factory lookup when no enclosing RLC checker exists. Start from checker.getParentChecker() or otherwise ignore the initial checker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java`
around lines 261 - 266, getRlccAtfIfPartOfHierarchy incorrectly begins scanning
at the current checker, causing a standalone MustCallChecker to be treated as
part of the RLC hierarchy; change the probe to skip the current checker by
initializing currentChecker to checker.getParentChecker() (or otherwise start
from the parent) and then iterate while currentChecker != null, checking
ResourceLeakUtils.rlcCheckers for currentChecker.getClass().getCanonicalName()
and calling ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this) only
when an enclosing RLC checker is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java`:
- Around line 293-337: The current loop in MustCallAnnotatedTypeFactory only
recurses into nested declared type arguments when
ResourceLeakUtils.isCollection(adt.getUnderlyingType()) is true, so declared
inner collections (e.g., Optional<List<?>>) aren't visited; change the logic so
that the handling that forces wildcard/typevar element annotations to BOTTOM
remains when the outer type is a collection, but the recursive call to
replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg)
is executed for any AnnotatedDeclaredType type argument regardless of the outer
isCollection check—i.e., move or duplicate the branch that checks
typeArg.getKind() == TypeKind.DECLARED out of the isCollection guard (or add a
second pass over adt.getTypeArguments() that calls
replaceCollectionTypeVarsWithBottomIfTop for DECLARED args) so nested declared
types are always descended into.

---

Duplicate comments:
In
`@checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java`:
- Around line 261-266: getRlccAtfIfPartOfHierarchy incorrectly begins scanning
at the current checker, causing a standalone MustCallChecker to be treated as
part of the RLC hierarchy; change the probe to skip the current checker by
initializing currentChecker to checker.getParentChecker() (or otherwise start
from the parent) and then iterate while currentChecker != null, checking
ResourceLeakUtils.rlcCheckers for currentChecker.getClass().getCanonicalName()
and calling ResourceLeakUtils.getRLCCalledMethodsAnnotatedTypeFactory(this) only
when an enclosing RLC checker is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5217275d-7bce-4d89-8cd5-3b72ed0670d4

📥 Commits

Reviewing files that changed from the base of the PR and between ae959a1 and b48eba3.

📒 Files selected for processing (2)
  • checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java
  • checker/tests/resourceleak-collections/ArraysAsListZeroArgs.java

Comment on lines +293 to +337
if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) {
for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) {
if (typeArg == null) {
continue;
}
if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) {
if (tree != null && tree instanceof NewClassTree) {
if (((NewClassTree) tree).getTypeArguments().isEmpty()) {
// Diamond [new Class()<>]. Not explicit generic type param.
// This will be inferred later. Don't put it to bottom here.
continue;
}
}
AnnotationMirror mcAnno = typeArg.getAnnotationInHierarchy(TOP);
boolean typeArgIsMcUnknown =
mcAnno != null
&& processingEnv
.getTypeUtils()
.isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType());
if (typeArgIsMcUnknown) {
// check whether the upper bounds have manual MustCallUnknown annotations, in which case
// they should
// not be reset to bottom. For example like this:
// void m(List<? extends @MustCallUnknown Object>) {}

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);
}
} else {
typeArg.replaceAnnotation(BOTTOM);
}
}
} else if (typeArg.getKind() == TypeKind.DECLARED) {
replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Recurse into nested declared type arguments even when the outer type is not a collection.

The ResourceLeakUtils.isCollection(...) guard currently wraps both the rewrite and the recursive descent. As a result, types like Optional<List<?>> or Holder<Iterator<?>> never visit their inner collection/iterator arguments, so those element types keep @MustCallUnknown instead of being forced to bottom.

♻️ Suggested fix
 private void replaceCollectionTypeVarsWithBottomIfTop(Tree tree, AnnotatedDeclaredType adt) {
-  if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) {
-    for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) {
+  boolean currentIsCollection = ResourceLeakUtils.isCollection(adt.getUnderlyingType());
+  for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) {
       if (typeArg == null) {
         continue;
       }
-      if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) {
+      if (currentIsCollection
+          && (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR)) {
         ...
       } else if (typeArg.getKind() == TypeKind.DECLARED) {
         replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg);
       }
-    }
   }
 }
📝 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
if (ResourceLeakUtils.isCollection(adt.getUnderlyingType())) {
for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) {
if (typeArg == null) {
continue;
}
if (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR) {
if (tree != null && tree instanceof NewClassTree) {
if (((NewClassTree) tree).getTypeArguments().isEmpty()) {
// Diamond [new Class()<>]. Not explicit generic type param.
// This will be inferred later. Don't put it to bottom here.
continue;
}
}
AnnotationMirror mcAnno = typeArg.getAnnotationInHierarchy(TOP);
boolean typeArgIsMcUnknown =
mcAnno != null
&& processingEnv
.getTypeUtils()
.isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType());
if (typeArgIsMcUnknown) {
// check whether the upper bounds have manual MustCallUnknown annotations, in which case
// they should
// not be reset to bottom. For example like this:
// void m(List<? extends @MustCallUnknown Object>) {}
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);
}
} else {
typeArg.replaceAnnotation(BOTTOM);
}
}
} else if (typeArg.getKind() == TypeKind.DECLARED) {
replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg);
boolean currentIsCollection = ResourceLeakUtils.isCollection(adt.getUnderlyingType());
for (AnnotatedTypeMirror typeArg : adt.getTypeArguments()) {
if (typeArg == null) {
continue;
}
if (currentIsCollection
&& (typeArg.getKind() == TypeKind.WILDCARD || typeArg.getKind() == TypeKind.TYPEVAR)) {
if (tree != null && tree instanceof NewClassTree) {
if (((NewClassTree) tree).getTypeArguments().isEmpty()) {
// Diamond [new Class()<>]. Not explicit generic type param.
// This will be inferred later. Don't put it to bottom here.
continue;
}
}
AnnotationMirror mcAnno = typeArg.getAnnotationInHierarchy(TOP);
boolean typeArgIsMcUnknown =
mcAnno != null
&& processingEnv
.getTypeUtils()
.isSameType(mcAnno.getAnnotationType(), TOP.getAnnotationType());
if (typeArgIsMcUnknown) {
// check whether the upper bounds have manual MustCallUnknown annotations, in which case
// they should
// not be reset to bottom. For example like this:
// void m(List<? extends `@MustCallUnknown` Object>) {}
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);
}
} else {
typeArg.replaceAnnotation(BOTTOM);
}
}
} else if (typeArg.getKind() == TypeKind.DECLARED) {
replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java`
around lines 293 - 337, The current loop in MustCallAnnotatedTypeFactory only
recurses into nested declared type arguments when
ResourceLeakUtils.isCollection(adt.getUnderlyingType()) is true, so declared
inner collections (e.g., Optional<List<?>>) aren't visited; change the logic so
that the handling that forces wildcard/typevar element annotations to BOTTOM
remains when the outer type is a collection, but the recursive call to
replaceCollectionTypeVarsWithBottomIfTop(null, (AnnotatedDeclaredType) typeArg)
is executed for any AnnotatedDeclaredType type argument regardless of the outer
isCollection check—i.e., move or duplicate the branch that checks
typeArg.getKind() == TypeKind.DECLARED out of the isCollection guard (or add a
second pass over adt.getTypeArguments() that calls
replaceCollectionTypeVarsWithBottomIfTop for DECLARED args) so nested declared
types are always descended into.

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@msridhar I agree the current structure is more scattered than ideal, and I also agree that the long-term direction should be to consolidate more of the orchestration under Collection Ownership checker. But I do not think the right end state is to move all of the logic there.

The refactoring I have in mind is not “move everything into Collection Ownership”, but rather: Collection Ownership should own the traversal bookkeeping/lifecycle, while MCCA remains the proof engine.

Concretely, I think a cleaner design would be to introduce a separate coordination layer owned by the Collection Ownership side. That layer would own the candidate loops (via own visitor, instead of MustCallVisitor)/resolved/verified loop state, CFG-resolution bookkeeping, and the API that CollectionOwnershipTransfer uses to query certified loops. Then, once a loop is resolved enough to be checked, that coordinator would delegate to MCCA for the actual certification.

I think the certification trigger still has to happen at the Called Methods post-analysis point, or at an equivalent lifecycle point where the Called Methods stores are already available. If we instead wait and do the certification in Collection Ownership’s own post-analysis, then the normal Collection Ownership transfer for that same analysis has already run by that point, so the certified result arrives too late to affect the current CO flow pass. In other words, doing certification there would mean we would need to run Collection Ownership flow again, or introduce some second pass just to consume the newly certified loops.

So in summary the proposed direction: add a CollectionOwnershipCoordinator to own bookkeeping for loops and the API used by CollectionOwnershipTransfer, while keeping certification in MCCA. Certification should still be triggered at CM postAnalyze, since that is when the Called Methods stores are definetly available (I can't think of a better point); doing it in CO post-analysis would require rerunning CO or adding a second pass.

@msridhar
Copy link
Copy Markdown
Contributor

@iamsanjaymalakar didn't we discuss overriding some pre-analysis hook in our meeting last week? What happened to that? I don't really know what you mean by a coordination layer. That is not standard terminology for the Checker Framework. Please be more concrete about what you are proposing.

@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@iamsanjaymalakar didn't we discuss overriding some pre-analysis hook in our meeting last week? What happened to that? I don't really know what you mean by a coordination layer. That is not standard terminology for the Checker Framework. Please be more concrete about what you are proposing.

@msridhar There is not currently a standard pre-analysis hook in the CF lifecycle. The relevant lifecycle points are: analyze(...), then postAnalyzeAfterFirstMethodAnalysis(...) for methods, and finally postAnalyze(...). As the pre-analysis hook needs to run before flow analysis, but after the CFG exists phase, I'll need to override GenericAnnotatedTypeFactory#analyze in CollectionOwnershipAnnotatedTypeFactory.

CollectionOwnershipAnnotatedTypeFactory will own a helper (CollectionOwnershipCoordinator class as mentioned above comment that does the book-keeping so that CollectionOwnershipAnnotatedTypeFactory remains clutter-free). Then overriding CollectionOwnershipAnnotatedTypeFactory.analyze(...) and in that override, once the CFG has been built but before analysis.performAnalysis(...) runs, CollectionOwnershipAnnotatedTypeFactory would call that helper to do the pre-analysis work for the current method.

One note: I can't just call super.analyze() from the overridden CollectionOwnershipAnnotatedTypeFactory#analyze as the hook needs to be after the cfg has been built but before analysis.performAnalysis(...). So I'll need to copy the implementation of GenericAnnotatedTypeFactory#analyze and insert the hook in between.

Let me know if this answers your question.

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.

5 participants