Skip to content

Introduce @UnannotatedFor - #856

Merged
wmdietl merged 39 commits into
eisop:masterfrom
aosen-xiong:jspecify-eisop
Sep 14, 2026
Merged

wmdietl merged 39 commits into
eisop:masterfrom
aosen-xiong:jspecify-eisop

Conversation

@aosen-xiong

@aosen-xiong aosen-xiong commented Aug 15, 2024 •

Copy link
Copy Markdown
Collaborator

Fixes #1292.

Merge together with eisop/jdk#144, which adds the checker-qual copy of @UnannotatedFor under java.base in the companion JDK repo: without it, check-jdk-consistency.sh fails with File missing in JDK: framework/qual/UnannotatedFor.java. Both branches are named jspecify-eisop so CI's related-repo checkout matches them automatically.

Summary

@UnannotatedFor is the inverse of @AnnotatedFor: it subtracts a declaration from an enclosing @AnnotatedFor scope, so a package or class can be checked as a whole while individual members opt out. It mirrors @AnnotatedFor in every respect — same @Target (TYPE, METHOD, CONSTRUCTOR, PACKAGE), same retention, @Repeatable with the same container shape, and an applyToSubpackages element with the same meaning.

The Nullness Checker aliases JSpecify's @NullUnmarked to it, alongside a @DefaultQualifier(Nullable.class, locations = UPPER_BOUND) that restores the unmarked upper-bound default. Without that second half, an enclosing @NullMarked's @DefaultQualifier would keep applying and a type variable of a @NullUnmarked method would still be bounded by @NonNull.

Resolving a declaration that carries both

Writing an @AnnotatedFor and an @UnannotatedFor that name the same checker is a contradiction. The one written first wins, and a conflicting.annotatedfor warning is issued on the declaration.

Source order rather than a fixed precedence, because one annotation can supply both kinds at once: @NullUnmarked aliases to an @UnannotatedFor and to a @DefaultQualifier, and conflicting @DefaultQualifiers are already resolved by source order (#2066). Were the two halves resolved by different rules, @NullUnmarked @NullMarked would leave the declaration in scope for checking while its upper-bound default came from the annotation that lost — a state neither annotation produces on its own, and one that -AonlyAnnotatedFor makes observable.

An aliasing annotation counts at its own position and a @Repeatable container at the position of its first repeat, matching the @DefaultQualifier resolution. The ordered pass runs only for a declaration carrying both, so the common path is unchanged.

conflicting.annotatedfor is exempt from @AnnotatedFor-scope suppression. Otherwise, when the @UnannotatedFor wins the declaration falls outside the scope and -AonlyAnnotatedFor or -AuseConservativeDefaultsForUncheckedCode=source would discard the warning — leaving the contradiction unreported in exactly one of its two orders. An explicit @SuppressWarnings still silences it.

Mechanism

AnnotatedTypeFactory.getUnannotatedForAnnotations is the @UnannotatedFor counterpart of getAnnotatedForAnnotations. Both delegate to getAllDeclAnnotations, which was generalized to take the @Repeatable container's name and value element, so the container unpacking is shared rather than duplicated; it now collects the annotation and its container in a single pass over the element's declaration annotations. doesAnnotatedForApplyToThisChecker and its @UnannotatedFor counterpart share appliesToThisChecker.

@UnannotatedFor is EISOP-specific, so the whole annotation is absent when the classpath resolves org.checkerframework.framework.qual from upstream typetools checker-qual. Its names are literal strings rather than class literals so referring to them does not link the class, and the resolved elements are null in that case, which makes every lookup return empty.

Known limitations

Tests

jtreg coverage for nesting, subpackage opt-in/opt-out, repeatability, and the @NullUnmarked alias, under no flag, -AonlyAnnotatedFor, -AuseConservativeDefaultsForUncheckedCode=source, and -Amode=jspecify. conflictingAnnotatedForPackage and conflictingUnannotatedForPackage cover both orders of the contradiction on a package; the second is what distinguishes source order from a fixed precedence, since under the latter its package would be checked and its argument would be an error.

🤖 Generated with Claude Code

https://claude.ai/code/session_012n7eKNuv1S2mTden7x56ie

@aosen-xiong aosen-xiong changed the title JSpecify - eisop nullness checker Introduce @UnAnnnotatedFor Jun 28, 2025
@aosen-xiong aosen-xiong changed the title Introduce @UnAnnnotatedFor Introduce @UnannnotatedFor Jun 28, 2025
@aosen-xiong

Copy link
Copy Markdown
Collaborator Author

TODO: change Retention for both annotatedfor and unannotatedfor qualifiers.

aosen-xiong and others added 5 commits October 12, 2025 12:48
Keep only the introduction of @UnannotatedFor.  Conflicts between
@AnnotatedFor and @UnannotatedFor on the same element are left for a separate
change, so drop what this branch had toward them:

 * SourceChecker: revert to master.  Its only effect was to let an
   @UnannotatedFor override an @AnnotatedFor on the same element; for every
   other element it returned what master already returned, and no test covers
   it.  It also declared an unused ANNOTATEDFOR_CONFLICT message key with no
   entry in any messages.properties, and iterated both annotation arrays after
   a null check that only required one of them to be non-null.

 * QualifierDefaults: remove a commented-out copy of
   isElementUnannotatedForThisChecker and restore a Javadoc typo.

The only merge conflict was an import in AnnotatedTypeFactory.
Conflict in QualifierDefaults: eisop#1331 removed isElementAnnotatedForThisChecker
and its elementAnnotatedFors cache from QualifierDefaults, so this branch's
changes to that method are obsolete.  Take master's side; the @UnannotatedFor
logic is ported to the shared lookup in a follow-up commit.

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

@UnannotatedFor was defined and given an AnnotatedTypeFactory helper, but its
effect was implemented in QualifierDefaults.isElementAnnotatedForThisChecker,
which eisop#1331 removed.  Implement it in the lookup that replaced it,
BaseTypeChecker.isElementAnnotatedForThisCheckerOrUpstreamChecker, so that it
governs both conservative defaults and warning suppression.

@AnnotatedFor is tested first, so @UnannotatedFor is purely subtractive: it
stops the walk to the enclosing element, and never overrides an explicit
@AnnotatedFor on the same element.  A nested @AnnotatedFor takes effect again.

Both shouldSuppressWarnings overloads accumulated the @AnnotatedFor answer over
every enclosing declaration, and the TreePath overload additionally asked about
the enclosing package.  That defeated the exclusion: an @UnannotatedFor class in
an @AnnotatedFor package was defaulted as unchecked code but still had its
warnings reported.  Ask isElementAnnotatedForThisCheckerOrUpstreamChecker once,
about the innermost declaration; it already resolves enclosing scopes, so the
per-level query was redundant work even before this change.

Only one cache is needed.  The element's cached boolean is the fully-resolved
scope answer, including @UnannotatedFor exclusions.

Also mirror doesUnannotatedForApplyToThisChecker on its @AnnotatedFor sibling,
and correct the @UnannotatedFor javadoc, which claimed an effect on bytecode
although the annotation has source retention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolve the conflicts that eisop#1996 (package annotation subpackage opt-outs)
and eisop#1998 (checker modes) create with @UnannotatedFor:

- BaseTypeChecker: eisop#1996 replaced the enclosing-package step of
  isElementAnnotatedForThisCheckerOrUpstreamChecker with a walk that honors
  applyToSubpackages, and @UnannotatedFor has to stop that walk too.  The
  walk is now tri-state (PackageScope), so the innermost package annotation
  wins whichever of the two it is.
- AnnotatedTypeFactory: keep both the applyToSubpackages element of
  @AnnotatedFor and the value element of @UnannotatedFor, and add the
  applyToSubpackages element of @UnannotatedFor.
- @UnannotatedFor gains the applyToSubpackages element that eisop#1996 added to
  @AnnotatedFor, @HasQualifierParameter and @ReportUse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aosen-xiong and others added 4 commits September 9, 2026 23:48
Fourteen findings from a review of the `@UnannotatedFor` branch.  The two
that mattered most were regressions this branch introduced:

- `AnnotatedTypeFactory` resolved `@UnannotatedFor`'s elements with
  `TreeUtils.getMethod`, which throws a `UserError` when the type is absent.
  `@UnannotatedFor` is EISOP-specific, so an upstream typetools `checker-qual`
  on the classpath aborted every checker before any file was checked, where
  that configuration previously worked with a degraded `applyToSubpackages`.
  `getMethodOrNull` is not a fix: it routes through the same `getMethods`.
- `@NullUnmarked` undid only the `@AnnotatedFor` half of `@NullMarked`, so a
  type variable of a `@NullUnmarked` method was still bounded by `@NonNull`.

Also: hoist a fast-path guard in both `SourceChecker.shouldSuppressWarnings`
overloads, which run for every reported diagnostic and are provably constant
`false` when neither `-AuseConservativeDefaultsForUncheckedCode=source` nor
`-AonlyAnnotatedFor` is set; correct the `@UnannotatedFor` and
`isElementAnnotatedForThisCheckerOrUpstreamChecker` javadoc; and add tests
for the conservative-defaulting half of `@UnannotatedFor`, which no test had
observed from outside an excluded scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconcile @UnannotatedFor with three changes that landed upstream:

- eisop#1304 (@NullMarked aliasing) solves the subchecker-divergence problem that
  this branch had solved by gating the alias on -Amode=jspecify.  Its fix is
  better: alias to @AnnotatedFor("nullnessnoinit"), so the alias's reach
  matches the factory it is registered on, in every mode.  Drop the gating
  and give @NullUnmarked the same treatment: @UnannotatedFor("nullnessnoinit",
  applyToSubpackages = false), plus the @DefaultQualifier half that undoes an
  enclosing @NullMarked's upper-bound default.
- eisop#2041 (@AnnotatedFor repeatable) replaced the single getDeclAnnotation call
  in BaseTypeChecker with a loop over getAnnotatedForAnnotations.  Do the same
  for @UnannotatedFor, in both the element walk and the subpackage walk.
- eisop#2040 removed the deprecated org.jspecify.nullness package, so drop the
  legacy aliases this branch had added for it.

Follow eisop#2041's guard style in AnnotatedTypeFactory: name UnannotatedFor and
UnannotatedFor.List with literal strings rather than class literals, so a
classpath without them is detected instead of linking them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The merge conflict resolution inserted the `@UnannotatedFor` sentence into
warnings.tex's `-AonlyAnnotatedFor` section twice, and left the
`NullnessChecker.addOptionsForMode` javadoc describing this branch's earlier,
superseded design, in which `-Amode=jspecify` alone made `@NullMarked` and
`@NullUnmarked` scope annotations and the alias named the "nullness" checker.
eisop#1304 aliases to "nullnessnoinit" in every mode instead.

Drop the duplicate sentence, and restore the javadoc to master's wording.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`getUnannotatedForAnnotations` collects written and aliased annotations
together so that a written `@UnannotatedFor` for another checker cannot hide
the `@NullUnmarked` alias, but no test exercised that composition; a
regression to a single `getDeclAnnotation` call would have passed every test.
The manual also called `@UnannotatedFor` the inverse of `@AnnotatedFor`
without saying that stub files ignore it, or which one wins when both are on
one element.

Add the composition case to NullUnmarkedScope.java, document both points, and
fix the `NullnessUnannotatedForTest` javadoc, which named the wrong class.
Also collapse the tail of both `shouldSuppressWarnings` overloads: after the
fast path returns early when neither flag is set, the `else if` on those
flags was always true and the final `return false` was unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aosen-xiong and others added 4 commits September 10, 2026 18:26
`:framework:checkSignature` rejected the `@FullyQualifiedName` local that
held `UnannotatedFor.List`'s name, because it was built by concatenating
".List" onto the `UnannotatedFor` name, and the Signature Checker types a
concatenation as `@SignatureUnknown`.

Write the name as a literal, as is already done for `UnannotatedFor` and for
`AnnotatedFor.List` just above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch added the JSpecify jar to the jtreg javac classpath so that the
NullUnmarkedPackage jtreg test could resolve `org.jspecify.annotations`.  The
change was never needed: jtreg's javac classpath already includes the
checker shadow jar, which bundles the JSpecify annotations.  With master's
build.gradle, all 116 checker jtreg tests pass, and NullUnmarkedPackage fails
with exactly its expected nullness diagnostic rather than an unresolved
import.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release section lists changes newest-first, but this branch's two
entries were added long ago and every PR merged since has stacked its entry
above them, leaving them in the middle of the section.  Move them to the top,
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in eisop#2022 (resolve aliases in checks that read annotations as
written).  That change is about type-qualifier aliases and does not touch
the declaration-alias path that `@UnannotatedFor` and `@NullUnmarked` use,
but it made `AnnotatedTypeFactory.getAllDeclAnnotations` private and
documented `getAnnotatedForAnnotations` as its only caller.
`getUnannotatedForAnnotations` is a second caller in the same class, so the
access change needs nothing, and the javadoc now names both.

The only textual conflict was the release's "Closed issues" list; keep both
sides' issue numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aosen-xiong
aosen-xiong marked this pull request as ready for review September 11, 2026 23:34
aosen-xiong and others added 2 commits September 11, 2026 19:34
…nnotatedFor`

`getAnnotatedForAnnotations` and `getUnannotatedForAnnotations` were the same
method apart from the names they look up: collect the written and aliased
annotations, then unpack the `@Repeatable` container.  `getAllDeclAnnotations`,
already private with only those two callers, now takes the container's name and
`value` element and does the unpacking, so each public method is one call.

Naming the container rather than passing its class also removes the last two
`.List` class literals: a class literal loads the type, which is what the
constructor's string names exist to avoid.  Both names become constants, so the
constructor and the lookup share one literal.

Also rename `namesThisChecker` to `appliesToThisChecker`, matching the public
methods it serves, group the four `does...` predicates by the question they
answer, and move the null-element guard into `appliesToThisChecker` so that
both `@UnannotatedFor` predicates read like their `@AnnotatedFor` twins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The walk returned a three-valued `PackageScope` on the grounds that a boolean
could not distinguish "an `@UnannotatedFor` excludes the subpackages" from
"nothing found here, keep looking".  It can: in a recursive walk, "keep
looking" is the recursive call, not a returned value.  Each package either
decides and returns, or hands back its parent's answer unchanged, so no level
ever needs to tell those two apart.

Restore master's boolean `doesAnnotatedForReachSubpackages`, its cache and its
name, and add one early return for an `@UnannotatedFor` that reaches
subpackages.  `checker/jtreg/subpackages/UnannotatedForNested` fails without
that early return, so it is what makes the nested case work.

Also merge `isElementUnannotatedForThisChecker` and `excludesSubpackages`,
which were the same loop apart from one conjunct, into one method taking a
`requireSubpackages` flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 11, 2026 23:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

aosen-xiong and others added 2 commits September 11, 2026 19:35
…ent diagnostics

`shouldSuppressWarnings(Element, String)` had to change with the `TreePath`
overload, or a diagnostic reported on an element would escape an
`@UnannotatedFor` exclusion through its three callers: a report on an element,
`InitializationVisitor`'s list of uninitialized fields, and whole-program
inference.  Nothing tested it: reverting that overload to master's logic passed
every test.  Extend `ElementSuppressionTestCase`, whose test checker reports on
class elements, with an excluded class and a sibling that is still checked.

Document which defaults apply in an `@UnannotatedFor` scope, which the manual
did not say: the conservative defaults under
`-AuseConservativeDefaultsForUncheckedCode=source`, the ordinary defaults under
`-AonlyAnnotatedFor` (which only suppresses warnings), and, in both cases, an
enclosing `@DefaultQualifier` still wins over the conservative defaults for the
hierarchy and locations it names --- including the one `@NullMarked` stands for.

Also record in the changelog that `@NullUnmarked`, like `@NullMarked`, is
retained in class files and so applies to bytecode, and correct a fixture
comment copied from the `@AnnotatedFor` opt-out test, where the same words
describe the opposite outcome.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes eisop#876.

Writing both annotations for one checker on a single declaration contradicts
itself.  The framework resolved it silently, in favor of `@AnnotatedFor`, so
the `@UnannotatedFor` had no effect and nothing said so.  Report
`conflicting.annotatedfor` instead; the precedence still applies if the warning
is suppressed.

Only a checker named by both annotations warns, so
`@AnnotatedFor("nullness") @UnannotatedFor("regex")` is silent.  The warning is
deduplicated through the ultimate parent checker: "nullness" matches the
upstream chain of `NullnessChecker`, `NullnessNoInitSubchecker` and
`KeyForSubchecker`, each of which has its own visitor and would otherwise
report the same conflict.  Restricting it to the parent checker instead would
miss `@NullMarked`/`@NullUnmarked`, which resolve only in the nullness
subchecker's factory.

A package is reached through a class in it, since a `package-info.java`
declares no type and the type processor never visits it; the warning is
reported on the package element, so it is positioned at the package
declaration rather than at the class that led to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aosen-xiong added a commit to aosen-xiong/jdk that referenced this pull request Sep 11, 2026
Keeps this repo's copy byte-identical to eisop/checker-framework's, per
`checker/bin-devel/check-jdk-consistency.sh`.

eisop/checker-framework#856 adds to the javadoc that writing both an
`@AnnotatedFor` and an `@UnannotatedFor` naming the same checker on one
declaration is a `conflicting.annotatedfor` warning, and that the
`@AnnotatedFor` wins if it is suppressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aosen-xiong and others added 5 commits September 11, 2026 20:08
Only the two documentation files conflicted, both because master and this
branch each added an entry at the top of the same section.  Keep both.

In nullness-checker.tex, master still carries "`@NullUnmarked` is not yet
honored", which this branch makes false; drop that sentence and keep both its
replacement and master's new `-AjspecifyUnrecognizedLocations` item (eisop#2024).

eisop#2044 reworks how a package's `@DefaultQualifier` propagates to subpackages,
which is the mechanism the `@NullMarked`/`@NullUnmarked` aliases rely on, both
being `applyToSubpackages = false` on the same location and hierarchy.  The
three behaviors it could have disturbed are each covered and pass: a
`@NullMarked` package not covering its subpackages, `@NullUnmarked` undoing the
upper-bound default, and `@NullUnmarked` on a package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test covered no flag, `-AonlyAnnotatedFor` and
`-AuseConservativeDefaultsForUncheckedCode=source`, but not the mode that
`@UnannotatedFor` exists to serve.  Add that run, as `AnnotatedForWithUse`
already does.

It gets its own reference file rather than sharing the `-AonlyAnnotatedFor`
one, matching `AnnotatedForWithUse`, whose two differ: its JSpecify-mode run
also reports `jspecify.unrecognized.location.local`.  The two files have the
same content here only because this fixture annotates no local variable.

Regenerate the three existing reference files as well: the added `@compile`
line shifts every line number in the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`processClassTree` reached the package through every class in it, and only the
report was deduplicated, so the package's `getAnnotatedForAnnotations` --- a
declaration-annotation scan, an alias-map lookup and a `.List` lookup --- ran
once per class rather than once per package, as the comment beside it claimed.
The check is not behind the flag fast path that guards scope resolution, so
this ran on every compilation.

Record the packages already examined, separately from the checker-wide record
of what has been reported: that one stops a second checker from repeating the
warning, this one stops the lookups from being repeated at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`BaseTypeChecker` and `BaseTypeVisitor` had five copies of the same loop:
iterate an element's `@AnnotatedFor` or `@UnannotatedFor` annotations --- which
may be several, since both are repeatable and an alias such as `@NullMarked`
adds another --- and ask whether any applies to this checker, in two of the
five also requiring that it apply to subpackages.

Replace them with `hasApplicableAnnotatedFor` and `hasApplicableUnannotatedFor`
on `BaseTypeChecker`, each taking a `requireSubpackages` flag.  Both callers are
in this package, so neither needs to be public.  `hasApplicable...` rather than
`is...ForThisChecker`, which read like `AnnotatedTypeFactory`'s
`does...ApplyToThisChecker`: those ask about one annotation, these about an
element.

The two comments that explained the loops move to the javadoc, including why
both conditions must hold of the same annotation.

`BaseTypeChecker.getTypeFactory()` returns the visitor's own factory, so moving
the loops off the visitor does not change which factory resolves aliases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The subpackages tests added by this PR followed the existing layout of that
directory: a top-level driver naming an abbreviated fixture package, with all
fixture trees flat beside each other. With five more tests the correspondence
between a driver and its fixtures stopped being evident from the listing --
UnannotatedForOptOutNested to uafoptout, NullUnmarkedPackage to nup.

Put each new test in its own directory holding its driver, its .out file and
its fixtures, as checker/jtreg/unboundedWildcards/issue1275,
checker/jtreg/stubs/fakeoverrides and framework/jtreg/issue845 already do. The
directory then carries the test's identity, so the fixture root is uniformly
`pkg` and fixture classes are named for the package they are in: InPkg, InSub,
Deep, Deeper.

The tests that predate this PR are left alone.

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

wmdietl commented Sep 14, 2026

Copy link
Copy Markdown
Member

#1482 updates the @AnnotatedFor retention. Should we either already update this annotation to RUNTIME or remember to also do that in 1482?

# Conflicts:
#	docs/CHANGELOG.md
#	framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java
Make UNANNOTATED_FOR_NAME protected in AnnotatedTypeFactory and use it instead of UnannotatedFor.class in NullnessNoInitAnnotatedTypeFactory. Also guard the applyToSubpackages element on nullUnmarkedUnannotatedForBuilder with unannotatedForApplyToSubpackagesElement != null, matching the pattern used for AnnotatedFor and DefaultQualifier.
@aosen-xiong

Copy link
Copy Markdown
Collaborator Author

#1482 updates the @AnnotatedFor retention. Should we either already update this annotation to RUNTIME or remember to also do that in 1482?

I think updating both @AnnotatedFor and @UnannotatedFor together would be better. So, let's update them together in #1482.

wmdietl and others added 2 commits September 14, 2026 07:45
An element carrying both, each naming this checker, was resolved by a fixed
precedence: the @AnnotatedFor won and the @UnannotatedFor was ignored. That
disagrees with how the same contradiction is resolved for @DefaultQualifier,
which the previous release made an error resolved by source order, and the
disagreement is reachable because one annotation can supply both kinds at
once: JSpecify's @NullUnmarked aliases to an @UnannotatedFor and to a
@DefaultQualifier(Nullable, UPPER_BOUND).

Writing @NullUnmarked before @NullMarked therefore produced a declaration
that was in scope for checking, because the @AnnotatedFor half won by fixed
precedence, but whose upper-bound default came from @NullUnmarked, because
the @DefaultQualifier half won by source order. Neither annotation produces
that combination on its own. Under -AonlyAnnotatedFor the class was checked
with unmarked defaults.

Resolve the pair by source order too, so both halves of such an alias agree.
AnnotatedTypeFactory#annotatedForPrecedesUnannotatedFor makes one ordered
pass over getDeclAnnotations and reports which kind it reaches first,
counting an aliasing annotation at its own position and a @repeatable
container at the position of its first repeat, as the @DefaultQualifier
resolution does. It runs only for an element carrying both, so the common
path is unchanged.

Reword conflicting.annotatedfor, which claimed the @UnannotatedFor "has no
effect": the annotation that loses here may still supply defaults that apply.

Also make the two new Element-keyed sets identity-keyed, as the neighboring
caches in BaseTypeChecker are and as the JDK documents Element requires; move
BaseTypeVisitor's new field up with the other fields; and note on
annotatedForReachesSubpackagesCache that its value now accounts for
@UnannotatedFor.

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

Resolving the pair by source order made the warning about it reachable only
in one of the two orders. The warning goes through shouldSuppressWarnings
like any other diagnostic, and under -AonlyAnnotatedFor or
-AuseConservativeDefaultsForUncheckedCode=source that method suppresses
everything outside an @AnnotatedFor scope. When the @UnannotatedFor is
written first it wins, the declaration is outside the scope, and the
contradiction was reported nowhere -- silently, and only for the order that
the previous commit changed.

Exempt conflicting.annotatedfor from the scope-based suppression: a
diagnostic about the @AnnotatedFor/@UnannotatedFor pair itself must not be
silenced by the scope those annotations define. An explicit
@SuppressWarnings, tested earlier in the same method, still silences it.

Update conflictingAnnotatedForPackage's expected output for the reworded
message; its own resolution is unchanged, since its @AnnotatedFor is written
first. Add conflictingUnannotatedForPackage for the other order, where the
package is left unchecked and only the warning is reported. That pair is what
distinguishes source order from a fixed precedence: with @AnnotatedFor always
winning, the new test's package would be checked and its argument would be an
error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wmdietl wmdietl changed the title Introduce @UnannnotatedFor Introduce @UnannotatedFor Sep 14, 2026
getAllDeclAnnotations walked the element's declaration annotations inline
and then called getDeclAnnotation for the @repeatable container, which walks
the same set again for the container's name and a third time for its
aliases, recomputing each mirror's name as it goes.

Handle both in the one loop, as getDefaultQualifierAnnotations does for
@DefaultQualifier and its container. This matters more now than when the
method served @AnnotatedFor alone: BaseTypeVisitor calls it for every class
and every method through the conflicting-annotation check, and it is called
a second time per element for @UnannotatedFor.

No behavior change: javac exposes either the individual annotations or the
container, never both, and the result is a set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wmdietl
wmdietl merged commit 026312a into eisop:master Sep 14, 2026
45 checks passed
@aosen-xiong
aosen-xiong deleted the jspecify-eisop branch September 14, 2026 18:17
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.

Add UnAnnotatedFor to simulate NullUnmarked annotation

3 participants