Skip to content

Resolve a type name in a local class's and an anonymous class's scope - #8185

Closed
mernst wants to merge 12 commits into
ajava-omit-irrelevantfrom
resolve-type-name-local-anonymous
Closed

mernst wants to merge 12 commits into
ajava-omit-irrelevantfrom
resolve-type-name-local-anonymous

Conversation

@mernst

@mernst mernst commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Based on #8184.

JavaParserUtil.resolveTypeName did not model the scope of a local class or of an anonymous class, so a name that such a class declares or inherits resolved to a same-named type declared outside it. If that type is irrelevant, inference silently discarded a correct annotation. TypeDeclaration.getFullyQualifiedName() is part of the problem: for a local class Foo in Outer it returns "Outer.Foo", which Elements resolves to a member type of Outer.

Model these scopes:

  • A local class shadows every type of the same name, and Elements cannot look up a local class, so return null (be conservative).
  • Likewise for a member type of a class that Elements cannot look up: a local class, an anonymous class (including the body of an enum constant), or a class nested within one.
  • Such a class also inherits its supertype's member types. Resolve the supertype and search it, which is precise rather than conservative. The supertype names are excluded from the class's own body scope, both because that is the Java rule and because it bounds the recursion.

Adds nameableFullyQualifiedName, which returns a fully-qualified name only when Elements can look it up.

Adds tests for a local class, an anonymous class, an enum constant's body, and a member type inherited into a local or anonymous class. Each test loses an annotation if its part of this fix is reverted.

🤖 Generated with Claude Code

mernst and others added 3 commits September 16, 2026 11:26
Resolving one type name looks up many candidate names -- one per enclosing
type declaration, one per import, one per on-demand import, one in the same
package, one in `java.lang`, and one for the name as a fully-qualified name.
Most of those names name no type, and a client that resolves many names looks
up the same names over and over.

Add an overload of `resolveTypeName` that takes a cache, and route every
lookup through it, so that a client that resolves many names pays for each
distinct name only once.  The cache records a lookup that finds no type, which
is the common case.  The existing one-argument overload is unchanged from a
caller's point of view; it allocates a cache that lives for the one call.

This is a performance change; it does not change any result.  A cache must not
be reused across annotation processing rounds or across `Elements` instances,
because a name that names no type in one round might name a generated type in
a later round; the Javadoc says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`JavaParserUtil.resolveTypeName` did not model the scope of a local class or
of an anonymous class, so a name that such a class declares or inherits
resolved to a same-named type that is declared outside it.  If that type is
irrelevant, inference silently discarded a correct annotation.
`TypeDeclaration.getFullyQualifiedName()` is part of the problem:  for a local
class `Foo` in `Outer`, it returns "Outer.Foo", which `Elements` resolves to a
member type of `Outer`.

Model these scopes:

 * A local class shadows every type of the same name, and `Elements` cannot
   look up a local class, so return null (that is, be conservative).
 * Likewise for a member type of a class that `Elements` cannot look up:  a
   local class, an anonymous class (including the body of an enum constant),
   or a class nested within one.
 * Such a class also inherits its supertype's member types.  Resolve the
   supertype and search it, which is precise rather than conservative.  The
   supertype names are excluded from the class's own body scope, both because
   that is the Java rule and because it bounds the recursion.

Add `nameableFullyQualifiedName`, which returns a fully-qualified name only
when `Elements` can look it up, in place of
`TypeDeclaration.getFullyQualifiedName()`.

Resolving a supertype's name multiplies the number of name lookups, so pass a
single cache from the ajava writer to every call, rather than letting each
call allocate one that it discards.

Add tests for a local class, an anonymous class, an enum constant's body, and
a member type inherited into a local or anonymous class.  Each test loses an
annotation if its part of this fix is reverted.  `InheritedTypeShadows` also
shows that the supertype search is precise:  an annotation on an inherited
member type that is irrelevant is still omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mernst and others added 9 commits September 16, 2026 12:50
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
…-mernst-branch-resolve-type-name-memoize into resolve-type-name-local-anonymous
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
…nst-branch-ajava-omit-irrelevant into resolve-type-name-local-anonymous
A class's member types are in scope in its body, but not in its header:
its annotations, its type parameter bounds, and its supertype names.
Likewise, the member types of an anonymous class are in scope only in
its body, not in the arguments to its constructor.  `resolveTypeName`
now tests whether the name appears in the body, rather than testing only
whether it is one of the supertype names.

Other fixes:

 * In `outer.new Inner() { ... }`, `Inner` is a member of the type of
   `outer` rather than a name that is resolved in the scope of the
   expression, so the anonymous class's supertype cannot be determined.
   Return null (conservative) rather than resolving `Inner` lexically.

 * An enum inherits the member type `EnumDesc` from its implicit
   supertype `java.lang.Enum`.  Search `java.lang.Enum` when the enum is
   unnameable, rather than letting the name fall through to an enclosing
   scope.

 * Pass `typeElementCache` to `typeToTypeMirror`, which otherwise
   allocated a fresh cache per call, so the field was never read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mernst mernst closed this Sep 17, 2026
@mernst
mernst deleted the resolve-type-name-local-anonymous branch September 17, 2026 19:13
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.

1 participant