Skip to content

Make Map.keySet() handle multiple arguments to @KeyFor (fixes #2358)#7363

Merged
smillst merged 21 commits into
typetools:masterfrom
Gaoyan1999:fix-issue-2358
Dec 18, 2025
Merged

Make Map.keySet() handle multiple arguments to @KeyFor (fixes #2358)#7363
smillst merged 21 commits into
typetools:masterfrom
Gaoyan1999:fix-issue-2358

Conversation

@Gaoyan1999

@Gaoyan1999 Gaoyan1999 commented Nov 6, 2025

Copy link
Copy Markdown
Contributor

Summary

Enhance @KeyFor propagation for Map.keySet() by modifying addComputedTypeAnnotations in KeyForAnnotatedTypeFactory.

Problem

The element type of Set returned by Map.keySet() only carried @KeyFor for the current variable (e.g., "sharedCounts1") and did not inherit @KeyFor values from the Map’s key type argument (e.g., "sharedBooks").

    Map<@KeyFor({"sharedBooks"}) String, Integer> sharedBooks = new HashMap<>();
    Map<@KeyFor({"sharedBooks"}) String, Integer> sharedCounts1 = new HashMap<>();
    Set<@KeyFor({"sharedBooks", "sharedCounts1"}) String> otherChars1 = sharedCounts1.keySet();

Solution

Override visitMethodInvocation in KeyForPropagationTreeAnnotator. When the tree is a Map.keySet() call, read @KeyFor values from the receiver Map’s key type argument and from the current Set element type, merge the names, and update the keySet() return type’s element with the combined @KeyFor.

Reference

#2358

@coderabbitai

coderabbitai Bot commented Nov 6, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Removes a test skip annotation and adds two in-source error annotation comments in a nullness test. Adds visitMethodInvocation(MethodInvocationTree, AnnotatedTypeMirror) and a private helper mergeKeyForFromMapReceiverIntoKeySetReturn(...) to KeyForPropagationTreeAnnotator to propagate @KeyFor from a Map receiver into a Set returned by Map.keySet(). Changes QualifierHierarchy.greatestLowerBoundQualifiers(...) visibility from protected to public.

Suggested reviewers

  • smillst

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent 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 25afdad and a7fa7f5.

📒 Files selected for processing (1)
  • checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (2 hunks)
🔇 Additional comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

148-149: Clarify or assert type argument checks.

As noted in a previous review, these checks handle raw types (e.g., Map without type parameters). Consider whether assertions would be more appropriate here, or add comments explaining that these guards handle raw-type receivers/returns.

Based on previous review feedback from mernst.

Also applies to: 155-156


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Gaoyan1999

Copy link
Copy Markdown
Contributor Author

Hi, @mernst @smillst, I would appreciate it if you could take a look when you have a chance, thanks in advance!

@smillst smillst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a good start. I have a few suggestions.

@smillst smillst assigned Gaoyan1999 and unassigned smillst Dec 3, 2025
@mernst mernst linked an issue Dec 4, 2025 that may be closed by this pull request

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

34-41: Minor cleanups: typo in Javadoc and unused receiverTree local.

Two small nits:

  1. In the class Javadoc you still have @UknownKeyFor (missing n) in the description of case 2; it should match the actual @UnknownKeyFor qualifier name.

  2. In visitMethodInvocation:

ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree.getMethodSelect());
if (receiverTree != null) {
  AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(tree);
  ...
}

receiverTree is only used for the null-check; getReceiverType(tree) already handles the case of an implicit/absent receiver, so you can simplify to:

AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(tree);
if (receiverType != null
    && receiverType.getKind() == TypeKind.DECLARED
    && !keySetReturnType.getTypeArguments().isEmpty()) {
  ...
}

This removes a redundant TreeUtils call and keeps the method slightly cleaner.

Also applies to: 114-123

📜 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 c86cce7 and 6ccedaa.

⛔ Files ignored due to path filters (2)
  • checker/tests/nullness/KeyForContainsKeyDemo.class is excluded by !**/*.class
  • checker/tests/nullness/KeyForMultiple.class is excluded by !**/*.class
📒 Files selected for processing (3)
  • checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (4 hunks)
  • framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java (3 hunks)
  • framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java (1)
  • KeyForPropagator (30-207)

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

134-192: Unify @KeyFor merge semantics; avoid redundant set-building vs GLB logic.

The merge helper currently does two different, partially overlapping things:

  • It explicitly collects mergedKeyForValues as the union of @KeyFor values from the Map key type and the Set element type (using a LinkedHashSet).
  • When setElementKeyFor != null, it ignores mergedKeyForValues and instead uses
    mergedKeyFor =
        factory.getQualifierHierarchy()
            .greatestLowerBoundQualifiers(mapKeyKeyFor, setElementKeyFor);
  • Only when setElementKeyFor == null do you actually call createKeyForAnnotationMirrorWithValue(mergedKeyForValues).

That leads to a few issues:

  • The explicit value-union (mergedKeyForValues) is effectively dead code in the common case where the element already has a @KeyFor annotation.
  • The single source of truth for how @KeyFor values are combined is unclear: sometimes it’s the manual union, sometimes it’s whatever QualifierHierarchy.greatestLowerBoundQualifiers does.
  • The mergedKeyForValues.isEmpty() check is almost never useful after you have a non-null mapKeyKeyFor and may skip a legitimate merge in edge cases where annotations carry “empty” value sets.

It would be clearer and less error‑prone to choose one approach:

  • Option A (manual union): Always build mergedKeyForValues and construct the new annotation from it, regardless of whether setElementKeyFor was null:
    List<String> mapKeyForValues =
        AnnotationUtils.getElementValueArray(mapKeyKeyFor, factory.keyForValueElement, String.class);
    Set<String> mergedKeyForValues = new LinkedHashSet<>(mapKeyForValues);
    if (setElementKeyFor != null) {
        mergedKeyForValues.addAll(
            AnnotationUtils.getElementValueArray(
                setElementKeyFor, factory.keyForValueElement, String.class));
    }
    if (!mergedKeyForValues.isEmpty()) {
        AnnotationMirror mergedKeyFor =
            factory.createKeyForAnnotationMirrorWithValue(mergedKeyForValues);
        setElementType.replaceAnnotation(mergedKeyFor);
    }
  • Option B (hierarchy‑driven): Rely solely on the QualifierHierarchy for @KeyFor merging (e.g., via greatestLowerBoundQualifiersOnly if appropriate) and drop the explicit mergedKeyForValues logic entirely.

Right now the method mixes both styles, which complicates reasoning about whether you truly get the combined @KeyFor({"sharedBooks", "sharedCounts1"}) behavior described in issue #2358 and the PR summary. Unifying around a single mechanism will make the behavior more transparent and easier to maintain.

📜 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 6ccedaa and b1e99e6.

📒 Files selected for processing (2)
  • checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (2 hunks)
  • framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java (0 hunks)
💤 Files with no reviewable changes (1)
  • framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: mernst
Repo: typetools/checker-framework PR: 7354
File: annotation-file-utilities/tests/LocalMultipleManyMethodsShifted.java:14-14
Timestamp: 2025-11-02T02:18:00.536Z
Learning: In the Checker Framework repository, when doing refactoring PRs (such as preferring isEmpty() over size() comparisons), test files should not be changed. Tests should remain stable to preserve their exact patterns for annotation processing and bytecode verification purposes.
Learnt from: kelloggm
Repo: typetools/checker-framework PR: 7166
File: checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java:767-798
Timestamp: 2025-10-22T20:43:32.957Z
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
Repo: typetools/checker-framework PR: 7166
File: checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java:767-798
Timestamp: 2025-10-22T20:43:32.957Z
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.
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (3)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java (1)
  • KeyForPropagator (30-207)
javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java (1)
  • AnnotationUtils (51-1512)
javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java (1)
  • TreeUtils (116-2967)

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

116-135: Redundant receiver check excludes implicit receivers.

This issue was previously identified: receiverTree is obtained but only used for a null check, while the actual receiver type comes from getReceiverType(tree). This unnecessarily restricts the merge to explicit receivers, excluding valid Map.keySet() calls with implicit receivers (e.g., this.keySet() in a class implementing Map).

Consider simplifying by removing the receiverTree check and relying solely on receiverType != null:

-      ExpressionTree receiverTree = TreeUtils.getReceiverTree(tree.getMethodSelect());
-      if (receiverTree != null) {
-        AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(tree);
-        if (receiverType != null
-            && receiverType.getKind() == TypeKind.DECLARED
-            && !keySetReturnType.getTypeArguments().isEmpty()) {
+      AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(tree);
+      if (receiverType != null
+          && receiverType.getKind() == TypeKind.DECLARED
+          && !keySetReturnType.getTypeArguments().isEmpty()) {
           AnnotatedDeclaredType receiverDeclaredType = (AnnotatedDeclaredType) receiverType;
           mergeKeyForFromMapReceiverIntoKeySetReturn(
               receiverDeclaredType, keySetReturnType, (KeyForAnnotatedTypeFactory) atypeFactory);
-        }
       }

Based on past review comment.

📜 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 b1e99e6 and c771ba7.

📒 Files selected for processing (1)
  • checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (2)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java (1)
  • KeyForPropagator (30-207)
javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java (1)
  • AnnotationUtils (51-1512)
⏰ 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 (inference_part1_jdk25)
  • GitHub Check: typetools.checker-framework (junit_jdk25)
  • GitHub Check: typetools.checker-framework (nonjunit_jdk25)
🔇 Additional comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

4-4: LGTM: Imports are appropriate and necessary.

All new imports are directly used in the new method implementations.

Also applies to: 7-10, 14-14, 19-19

@Gaoyan1999 Gaoyan1999 requested a review from smillst December 17, 2025 12:10
@Gaoyan1999

Copy link
Copy Markdown
Contributor Author

Hi @smillst, I’ve addressed the comments and left a note about an issue I ran into. I’d appreciate it if you could take a look.

@smillst smillst assigned smillst and unassigned Gaoyan1999 Dec 17, 2025
smillst
smillst previously approved these changes Dec 17, 2025
@smillst smillst enabled auto-merge (squash) December 17, 2025 17:13

@mernst mernst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is very nice. Thank you!

smillst
smillst previously approved these changes Dec 17, 2025

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

175-198: Simplify merge logic and remove redundant operations.

The current implementation has several inefficiencies previously flagged:

  1. Lines 175–182: mergedKeyForValues is manually constructed, but when setElementKeyFor != null (line 189), greatestLowerBoundQualifiers already handles merging KeyFor values, making this collection unnecessary for that branch.

  2. Lines 185–187: The empty check is unreachable. Since mapKeyKeyFor is verified non-null (line 163), mapKeyForValues must be non-empty, so mergedKeyForValues cannot be empty.

  3. Lines 196–198: When setElementKeyFor is null, no values are added at lines 178–182, so mergedKeyForValues equals mapKeyForValues. You can directly use mapKeyKeyFor via setElementType.replaceAnnotation(mapKeyKeyFor) instead of recreating an equivalent annotation.

Apply this diff to simplify:

     // Extract KeyFor annotation from the Set's element type.
     AnnotationMirror setElementKeyFor = setElementType.getEffectiveAnnotation(KeyFor.class);
 
-    // Collect all KeyFor values.
-    Set<String> mergedKeyForValues = new LinkedHashSet<>(mapKeyForValues);
-
-    if (setElementKeyFor != null) {
-      mergedKeyForValues.addAll(
-          AnnotationUtils.getElementValueArray(
-              setElementKeyFor, factory.keyForValueElement, String.class));
-    }
-
-    // Create a new KeyFor annotation with merged values.
-    if (mergedKeyForValues.isEmpty()) {
-      return;
-    }
     AnnotationMirror mergedKeyFor;
     if (setElementKeyFor != null) {
       // Use greatestLowerBoundQualifiers to merge the annotations.
       mergedKeyFor =
           factory
               .getQualifierHierarchy()
               .greatestLowerBoundQualifiers(mapKeyKeyFor, setElementKeyFor);
     } else {
-      // If setElementKeyFor is null, just use the mapKeyKeyFor (but we still need to create
-      // a new annotation with the merged values in case there are additional values).
-      mergedKeyFor = factory.createKeyForAnnotationMirrorWithValue(mergedKeyForValues);
+      // If setElementKeyFor is null, just use the mapKeyKeyFor.
+      mergedKeyFor = mapKeyKeyFor;
     }
     if (mergedKeyFor != null) {
       setElementType.replaceAnnotation(mergedKeyFor);
     }

Based on learnings, this is a duplicate of a previous review comment on the same logic.

📜 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 9e81daa and 25afdad.

📒 Files selected for processing (1)
  • checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (3)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java (1)
  • KeyForPropagator (30-207)
framework/src/main/java/org/checkerframework/framework/type/treeannotator/TreeAnnotator.java (1)
  • TreeAnnotator (30-100)
javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java (1)
  • AnnotationUtils (51-1512)
⏰ 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 (junit_jdk25)
  • GitHub Check: typetools.checker-framework (misc_jdk25)
  • GitHub Check: typetools.checker-framework (typecheck_part1_jdk25)
  • GitHub Check: typetools.checker-framework (nonjunit_jdk25)
  • GitHub Check: typetools.checker-framework (inference_part2_jdk25)
  • GitHub Check: typetools.checker-framework (inference_part1_jdk25)
🔇 Additional comments (1)
checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java (1)

149-150: Consider using assertions for empty type argument checks.

The early-return checks for empty type arguments may be unreachable. Since isCallToKeyset verifies this is a Map.keySet() call, both the Map and returned Set should have non-empty type arguments unless raw types are involved. If raw types are the concern, add a comment explaining this; otherwise, consider converting these to assertions to catch unexpected states during development.

Based on learnings, this addresses mernst's comment: "If this is a call to keySet(), how could the type arguments be empty? (Because of raw types?? In that case, please add a comment.) Would it be better to write an assertion rather than putting a test in the if?"

Also applies to: 156-157

@mernst mernst changed the title Fix issue #2358 Make Map.keySet() handle multiple argments to @KeyFor Map.keySet() handle multiple arguments to @KeyFor (fixes #2358) Dec 17, 2025
@mernst mernst changed the title Map.keySet() handle multiple arguments to @KeyFor (fixes #2358) Make Map.keySet() handle multiple arguments to @KeyFor (fixes #2358) Dec 17, 2025
@Gaoyan1999

Copy link
Copy Markdown
Contributor Author

Could someone rerun the pipeline? Thanks in advance.

@smillst smillst self-assigned this Dec 18, 2025
@smillst smillst merged commit 77ea6d6 into typetools:master Dec 18, 2025
21 checks passed
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.

Make Map.keySet() handle multiple argments to @KeyFor

3 participants