Skip to content

Check an annotated anonymous class creation against the declaration bound on all JDKs - #2027

Merged
wmdietl merged 11 commits into
eisop:masterfrom
aosen-xiong:jdk11-anon-bounds
Sep 11, 2026
Merged

wmdietl merged 11 commits into
eisop:masterfrom
aosen-xiong:jdk11-anon-bounds

Conversation

@aosen-xiong

@aosen-xiong aosen-xiong commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2009.

new @A AClass() {} is validated against AClass's declaration bound on Java 17 and later but not
on Java 11, so the same source checks differently depending on the compiler. #1909 unified how
anonymous classes are recognized, but this path was left behind.

Two things block it on Java 11, and neither alone changes the result:

TreeUtils.isTypeTree answered from the tree kind alone, which the TODO it carried admitted is an
under-approximation. BaseTypeValidator uses it to decide whether to validate a type that is
syntactically an expression, and on Java 11 the extends clause of an annotated anonymous class is a
bare identifier, so validation was skipped. It now resolves what the identifier, or member select,
refers to. The member-select case has no test: an annotated qualified type is Outer.@Anno Inner,
which arrives as an ANNOTATED_TYPE and was already handled, so only the unannotated form reaches
that branch and it has no annotation to validate.

The clause also carries no annotation on Java 11: javac attaches it to the anonymous class body's
modifiers rather than to the clause, a difference getExplicitNewClassAnnos already reconciles.
getTypeOfExtendsImplements applies those annotations to the clause's type, after
addComputedTypeAnnotations, whose defaulting would otherwise overwrite them.

Note for reviewers: isTypeTree is public API

TreeUtils.isTypeTree is public and its contract changes here. It was a pure kind test, answerable
for any tree; it now resolves an identifier or member select through elementFromTree, so it
requires an attributed tree. A caller that passes an unattributed tree gets a different answer than
before.

Inside this repository there are exactly two callers, and only one is affected:

  • GenericAnnotatedTypeFactory.getAnnotatedTypeLhs calls it in the default: branch of a switch
    on the tree kind. IDENTIFIER and MEMBER_SELECT are handled by earlier cases in that switch and
    never reach default:, so the new behaviour cannot be observed there.
  • BaseTypeValidator.shouldCheckTopLevelDeclaredOrPrimitiveType is the intended target, and is
    where the anonymous-class fix takes effect.

🤖 Generated with Claude Code

aosen-xiong and others added 2 commits September 6, 2026 10:59
…ound on Java 11

"new @A AClass() {}" is validated against AClass's declaration bound on Java 17
and later but not on Java 11, so the same source checks differently depending
on the compiler.  eisop#1909 unified how anonymous classes are recognized but
this path was left behind.

Two things block it on Java 11, and neither alone changes the result:

TreeUtils.isTypeTree answered from the tree kind alone, which the TODO it
carried admitted is an under-approximation.  BaseTypeValidator uses it to
decide whether to validate a type that is syntactically an expression, and on
Java 11 the extends clause of an annotated anonymous class is a bare
IdentifierTree, so validation was skipped.  Resolve the ambiguity by asking
what the identifier, or member select, refers to.

The clause also carries no annotation on Java 11: javac attaches it to the
anonymous class body's modifiers rather than to the clause, a difference
getExplicitNewClassAnnos already reconciles.  Apply those annotations to the
clause's type in getTypeOfExtendsImplements, after addComputedTypeAnnotations,
whose defaulting would otherwise overwrite them.

The annotation therefore lands on the extends clause, which is the second of
the two desugarings in eisop#2010.  That issue conditions it on whether the
type system permits annotations on supertypes; this change does not consult
that condition.

Fixes eisop#2009.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
applyAnonymousClassCreationAnnos guarded the TreePath and then dereferenced
getParentPath() unguarded.  An extends or implements clause always has a
parent, so the crash is unreachable, but the asymmetry reads as an oversight;
AnnotatedTypeFactory:4801 and :4807 guard the same call.

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

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.

# Conflicts:
#	docs/CHANGELOG.md
@wmdietl wmdietl changed the title Check an annotated anonymous class creation against the declaration bound on Java 11 Check an annotated anonymous class creation against the declaration bound on all JDKs Sep 8, 2026
wmdietl and others added 4 commits September 10, 2026 16:54
…type

AnonymousClassBounds.java covered a plain and a qualified extends clause
(IDENTIFIER and MEMBER_SELECT), but not an implements clause or a
PARAMETERIZED_TYPE clause. The parameterized case is the only shape that
isolates the AnnotatedTypeFactory half of the fix from the
TreeUtils.isTypeTree half: PARAMETERIZED_TYPE is already one of
typeTreeKinds() on every JDK, so only the annotation-reconciliation part is
exercised there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV
The CHANGELOG and a code comment said the annotation-placement quirk was a
Java 11 problem, checked correctly starting on Java 17. Probing javac's own
AST directly, across every JDK from 8 to 21, shows the actual boundary is
JDK 12: 8 through 11 attach the annotation to the anonymous class body's
modifiers, 12 through 21 attach it to the extends/implements clause
directly. Java 17 was never the boundary; nothing changed between 12 and
17 for this.

Also mark the reconciliation code in getExplicitNewClassAnnos as removable
once the minimum supported JDK is 12, since at that point it becomes a
no-op that only ever contributes an empty annotation list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV
…nnos

This file's convention for a void method that computes annotations and
stamps them onto an AnnotatedTypeMirror is "add*", even when it does so via
replaceAnnotations: addAnnotationFromFieldInvariant is the same shape
(void, computes annotations, calls type.replaceAnnotations(...)) and is
named with "add". "apply*" in this file is otherwise reserved for a method
that returns a transformed type (applyCaptureConversion), which is not
this method's shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV

@wmdietl wmdietl 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.

Thanks! Please look at my refactorings.

@wmdietl wmdietl assigned aosen-xiong and unassigned wmdietl Sep 10, 2026
@aosen-xiong

Copy link
Copy Markdown
Collaborator Author

Thanks! This looks good.

@aosen-xiong aosen-xiong assigned wmdietl and unassigned aosen-xiong Sep 10, 2026
@wmdietl
wmdietl enabled auto-merge (squash) September 11, 2026 20:12
@wmdietl
wmdietl merged commit 759caaf into eisop:master Sep 11, 2026
44 checks passed
@aosen-xiong
aosen-xiong deleted the jdk11-anon-bounds branch September 11, 2026 21:23
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.

An annotation on an anonymous class creation is not checked against the declaration bound on JDK 11

3 participants