Skip to content

Make checkPurity pass for javacutils. - #8262

Open
smillst wants to merge 14 commits into
typetools:masterfrom
smillst:purity-checker-checker-annos
Open

smillst wants to merge 14 commits into
typetools:masterfrom
smillst:purity-checker-checker-annos

Conversation

@smillst

@smillst smillst commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

I've added annotations for javacutil, and the smaller subprojects.

Merge typetools/jdk#299 first.

smillst and others added 7 commits September 21, 2026 09:07
Checking purity annotations is the entire purpose of the Purity Checker, so
running it no longer requires also supplying -AcheckPurityAnnotations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Now that the Purity Checker implies -AcheckPurityAnnotations, `checkPurity`
verifies purity annotations rather than trusting them, and javacutil reported
about 100 errors.  It now reports none.

Most are fixed by writing the annotation that was missing:

  * @SideEffectFree on BugInCF's constructors and @pure on getLocation().  This
    alone fixed 14 errors, because `throw new BugInCF(...)` appears in many
    methods that are declared pure.
  * @pure on ten TreeUtils methods, each of which reads a tree or symbol field.
  * @pure on CheckerFrameworkAnnotationValue.getValue(), and @SideEffectFree on
    DefaultAnnotationFormatter's constructor.

The rest are suppressed, and the suppressions are concentrated in one
structural limitation: the Purity Checker has no escape analysis.  It therefore
cannot verify:

  * a @deterministic method that iterates a collection, because
    Collection.iterator() and Map.keySet()/entrySet() allocate a fresh iterator
    or view -- so they are @SideEffectFree but not @deterministic -- and
    Iterator.next() mutates the iterator;
  * a @SideEffectFree method that mutates a locally allocated StringBuilder,
    StringJoiner, or TreeSet, or that memoizes its result in a field;
  * a @deterministic method containing a catch block.

Because java.util.Map and java.util.Set declare containsKey, get, contains,
equals, and hashCode as @pure, and purity annotations are inherited, every
collection implementation inherits an obligation that it cannot discharge.
AnnotationMirrorMap takes a class-level suppression for that reason, since all
of its inherited pure methods are affected; elsewhere the suppressions are
per-method.  This is concretely the "high false positive rate" that the manual
cites for not enabling -AcheckPurityAnnotations by default, and it is what the
new purity analysis promised there would need to address.

Two annotations are retained with a suppression rather than removed, because
each is load-bearing for type refinement in callers:
AnnotationUtils.annotationName (Name.toString() returns a fresh but always-equal
String) and AbstractTypeProcessor.getCompilerLog (Log.instance() registers the
Log in the compiler Context on its first call).  Dropping @SideEffectFree from
the latter costs three nullness errors in CFGProcessor, whose fields lose their
refinement across the call.

`typecheck-part1` runs `checkPurity` only on javacutil; the other subprojects do
not pass it yet.  Each still has its own `checkPurity` task to run directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hecker

This clears 21 errors across 6 files, and brings the number of subprojects that
pass `checkPurity` from one to five: checker-qual-android needed no edit, because
its 14 errors were in generated copies of the two checker-qual files fixed here.

ConversionCategory.toString() was declared @pure, which contradicts the rule that
the annotated JDK records in a @CFComment on Object.toString():  a toString() that
builds its result returns a string that differs according to ==, and
@deterministic requires that two calls return == results.  It is now
@SideEffectFree.  That was a wrong annotation rather than a false positive.

The other errors are the structural limitation described in the previous commit:
the Purity Checker has no escape analysis, so it cannot verify a method that
mutates a locally allocated StringBuilder, StringJoiner, or list, that calls
String.format or Character.toString (each of which returns a fresh string), or
that contains a catch block.

Three of RegexUtil's suppressions already explained this reasoning -- one
comment reads "temp value used in pure method is equal up to equals but not up
to ==" -- so "purity" joins the existing @SuppressWarnings lists rather than
arriving as a separate annotation, and each key now carries its own
justification.

In framework-test, both errors reached the same helper, so annotating the chain
(TestUtilities.optionMapToList, SimpleOptionMap.getOptionsAsList, and
ImmutableTestConfiguration.getFlatOptions as @SideEffectFree) needs one
suppression instead of two and gives those methods a correct specification.

`typecheck-part1` now excludes `checkPurity` by naming the four subprojects that
still fail -- annotation-file-utilities, checker, dataflow, and framework -- so
that a subproject is covered as soon as it passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
annotationName returns `elm.getQualifiedName().toString()`, a freshly built
string, so it is not deterministic in the sense @deterministic requires.  The
annotated JDK records that rule in a @CFComment on Object.toString():  such a
method "returns a string that differs according to ==, and @deterministic
requires that the results of two calls of the method are ==."

compareByName is weakened for the same reason, because it compares the results
of two annotationName calls.  Weakening it cascades no further:
areSameByName(AnnotationMirror, AnnotationMirror) has no purity annotation, and
compareAnnotationMirrors already needs its own suppression for the local TreeSet
that it builds.

This removes a suppression rather than adding one.  The @pure annotation on
annotationName required suppressing the determinism warning on its own body,
which amounted to documenting that the annotation was not true; the accurate
annotation needs no suppression at all.

Verified that nothing regresses: `typecheck-part1` and `typecheck-part2` (all
nine checkers) pass with no errors for every subproject that runs checkPurity,
and `checkNullness` still passes for framework and checker, which is where the
weaker specification could have cost type refinement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@smillst smillst self-assigned this Sep 22, 2026
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: typetools/checker-framework/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4ae79d9c-91c3-4493-acca-a35b91d4af80

📥 Commits

Reviewing files that changed from the base of the PR and between e94e605 and e7ac111.

📒 Files selected for processing (26)
  • SKIP-REQUIRE-JAVADOC
  • build.gradle
  • checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java
  • checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java
  • checker-util/src/main/java/org/checkerframework/checker/formatter/util/FormatUtil.java
  • checker-util/src/main/java/org/checkerframework/checker/regex/util/RegexUtil.java
  • checker/tests/lock-records/LockRecord.java
  • checker/tests/nullness/java8/Issue579.java
  • framework-test/src/main/java/org/checkerframework/framework/test/ImmutableTestConfiguration.java
  • framework-test/src/main/java/org/checkerframework/framework/test/SimpleOptionMap.java
  • framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java
  • framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/DiagnosticKind.java
  • framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java
  • framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnostic.java
  • framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticLine.java
  • framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java
  • javacutil/src/main/java/org/checkerframework/javacutil/AbstractTypeProcessor.java
  • javacutil/src/main/java/org/checkerframework/javacutil/AnnotationBuilder.java
  • javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorMap.java
  • javacutil/src/main/java/org/checkerframework/javacutil/AnnotationMirrorSet.java
  • javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java
  • javacutil/src/main/java/org/checkerframework/javacutil/BugInCF.java
  • javacutil/src/main/java/org/checkerframework/javacutil/DefaultAnnotationFormatter.java
  • javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java
  • javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava11.java
  • javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java
💤 Files with no reviewable changes (2)
  • SKIP-REQUIRE-JAVADOC
  • checker/tests/lock-records/LockRecord.java

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The typecheck-part1 task now depends on checkPurity except for four named projects. The change adds or adjusts purity annotations and warning suppressions in checker utilities, framework test utilities, and javac utilities. The described Java method implementations remain unchanged.

Suggested reviewers: mernst

Priority: ⬇️ Low

Change: Other

Merge Risk: ⚪ Minimal · up to e7ac1

This change adds purity annotations and warning suppressions and enables purity checking in the build for most subprojects. Runtime behavior is unchanged, and no concrete defects were found. As stated in the description, the companion JDK annotation change should be merged first so that the updated test expectations match.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.61% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 82 functions across 24 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 75.61% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 82 functions across 24 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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.

@smillst smillst changed the title Make the Purity Checker always pass -AcheckPurityAnnotations Make checkPurity pass for javacutils. Sep 22, 2026
smillst and others added 7 commits September 23, 2026 08:18
Comparator.thenComparing is @SideEffectFree in the annotated JDK, so the
overriding method in Issue579 inherits that annotation and may only call
side-effect-free constructors.
…y Checker

Methods annotated @SideEffectsOnly are now checked, so their callees need
purity annotations.
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.

2 participants