From f2fc99e6348bd10148dcc852623ab4a194b2d91a Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Mon, 12 Jan 2026 20:02:42 -0800 Subject: [PATCH] Road grading Signed-off-by: Laird Nelson --- README.md | 19 +- .../construct/BlockingCompilationTask.java | 21 +- .../org/microbean/construct/Processor.java | 80 ++- .../construct/element/AnnotationMirrors.java | 198 +++++++ .../element/SameAnnotationValueVisitor.java | 262 +++++++++ .../construct/element/StringName.java | 44 +- .../element/SyntheticAnnotationMirror.java | 210 ++------ .../SyntheticAnnotationTypeElement.java | 506 ++++++++++++++---- .../element/SyntheticAnnotationValue.java | 212 ++++++++ .../construct/element/SyntheticName.java | 38 +- .../element/UniversalAnnotation.java | 5 +- .../element/UniversalAnnotationValue.java | 47 +- .../construct/element/UniversalDirective.java | 17 +- .../construct/element/UniversalElement.java | 17 +- .../construct/type/UniversalType.java | 12 +- .../construct/element/TestTypes.java | 79 +++ 16 files changed, 1392 insertions(+), 375 deletions(-) create mode 100644 src/main/java/org/microbean/construct/element/AnnotationMirrors.java create mode 100644 src/main/java/org/microbean/construct/element/SameAnnotationValueVisitor.java create mode 100644 src/main/java/org/microbean/construct/element/SyntheticAnnotationValue.java create mode 100644 src/test/java/org/microbean/construct/element/TestTypes.java diff --git a/README.md b/README.md index 21d9d20..680e33d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ The microBean™ Construct project provides classes and interfaces assisting with Java constructs such as types and elements. +Among other things, this project enables constructs modeled by the `javax.lang.model.*` packages to be accessed at +runtime in a thread-safe manner. This permits the same constructs to be used at build time (e.g. by annotation +processors) and at runtime (e.g. by tools and environments that need to perform reflection-like activities without +actually loading and initializing classes), using only official and sanctioned APIs. + # Status This project is currently experimental, in a pre-alpha state, and unsuitable for production use. @@ -20,8 +25,8 @@ microBean™ Construct requires a Java runtime of version 21 or higher. # Installation -microBean™ Construct is, or will be, available on [Maven Central](https://search.maven.org/). Include microBean™ Construct -as a Maven dependency: +microBean™ Construct is available on [Maven Central](https://search.maven.org/). Include microBean™ Construct as a Maven +dependency: ```xml @@ -31,11 +36,17 @@ as a Maven dependency: Always check https://search.maven.org/artifact/org.microbean/microbean-construct for up-to-date available versions. --> - 0.0.18 + 0.0.19 ``` # Documentation -Full documentation is, or will be, available at +Full documentation is available at [microbean.github.io/microbean-construct](https://microbean.github.io/microbean-construct/). + +# References + +* This project is tangentially and purely coincidentally related to [JEP 119](https://openjdk.org/jeps/119). +* A seemingly shelved attempt to implement the `javax.lang.model.*` constructs in terms of reflective constructs can be + found in [JDK-8004133](https://bugs.openjdk.org/browse/JDK-8004133). diff --git a/src/main/java/org/microbean/construct/BlockingCompilationTask.java b/src/main/java/org/microbean/construct/BlockingCompilationTask.java index 0b6bfdd..623b751 100644 --- a/src/main/java/org/microbean/construct/BlockingCompilationTask.java +++ b/src/main/java/org/microbean/construct/BlockingCompilationTask.java @@ -130,6 +130,20 @@ public final void close() { } } + /** + * Overrides {@link CompletableFuture#completeExceptionally(Throwable)} to additionally {@linkplain #close() close} + * this {@link BlockingCompilationTask}. + * + * @param t a {@link Throwable}; see {@link CompletableFuture#completeExceptionally(Throwable)} for additional + * contractual obligations + * + * @return the result of invoking {@link CompletableFuture#completeExceptionally(Throwable)} with the supplied {@link + * Throwable} + * + * @see #close() + * + * @see CompletableFuture#completeExceptionally(Throwbale) + */ @Override // CompletableFuture public final boolean completeExceptionally(final Throwable t) { final boolean result = super.completeExceptionally(t); @@ -185,8 +199,7 @@ public final void run() { task.addModules(additionalRootModuleNames); // Set the task's annotation processor whose only function will be to return the ProcessingEnvironment supplied to - // it in its #init(ProcessingEnvironment) method. The supplied latch is used to make this task block forever (unless - // an error occurs) to keep the ProcessingEnvironment "in scope". + // it in its #init(ProcessingEnvironment) method. task.setProcessors(List.of(this.p)); if (LOGGER.isLoggable(DEBUG)) { @@ -196,8 +209,8 @@ public final void run() { } try { - final Boolean result = task.call(); // blocks forever deliberately unless an error occurs; see Processor2 - if (!TRUE.equals(result)) { + final Boolean result = task.call(); // blocks forever deliberately unless an error occurs; see Processor + if (!TRUE.equals(result)) { // (result could be null, technically speaking) if (LOGGER.isLoggable(ERROR)) { LOGGER.log(ERROR, "Calling CompilationTask failed"); } diff --git a/src/main/java/org/microbean/construct/Processor.java b/src/main/java/org/microbean/construct/Processor.java index a6dea9f..20e0cff 100644 --- a/src/main/java/org/microbean/construct/Processor.java +++ b/src/main/java/org/microbean/construct/Processor.java @@ -16,7 +16,6 @@ import java.lang.System.Logger; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.Condition; @@ -38,26 +37,55 @@ import static java.lang.System.getLogger; +import static java.util.Objects.requireNonNull; + final class Processor implements AutoCloseable, javax.annotation.processing.Processor { private static final Logger LOGGER = getLogger(Processor.class.getName()); + // @GuardedBy("lock") private final Consumer cpe; + // @GuardedBy("lock") // run() method invoked under lock private final Runnable r; private final Lock lock; + // @GuardedBy("lock") private final Condition c; // @GuardedBy("lock") private boolean closed; - Processor(final Consumer cpe, - final Runnable r) { + /** + * Creates a new {@link Processor}. + * + *

This {@link Processor} will be invoked by a {@link BlockingCompilationTask} as part of an invocation of its + * {@link BlockingCompilationTask#run()} method. Its {@link #init(ProcessingEnvironment)} method will be invoked as + * part of {@linkplain javax.annotation.processing.Processor the standard Processor lifecycle}. It will + * call the {@link Consumer#accept(Object) accept(ProcessingEnvironment)} method on the supplied {@link + * Consumer}. Then it will block until {@link #close()} is invoked (from a separate thread, obviously). Before + * exiting, it will invoke the {@link Runnable#run() run()} method of the supplied {@link Runnable}.

+ * + * @param cpe a {@link Consumer} of {@link ProcessingEnvironment} instances, typically {@link + * BlockingCompilationTask#complete(Object)}; must not be {@code null} + * + * @param r a {@link Runnable} that is invoked at the conclusion of an invocation of the {@link + * #init(ProcessingEnvironment)} method; may be {@code null} + * + * @see #init(ProcessingEnvironment) + * + * @see #close() + * + * @see BlockingCompilationTask + * + * @see javax.annotation.processing.Processor + */ + Processor(final Consumer cpe, // usually BlockingCompliationTask::complete + final Runnable r) { // usually BlockingCompliationTask::obtrudeException super(); - this.cpe = Objects.requireNonNull(cpe, "cpe"); + this.cpe = requireNonNull(cpe, "cpe"); this.r = r == null ? Processor::sink : r; this.lock = new ReentrantLock(); this.c = this.lock.newCondition(); @@ -83,12 +111,15 @@ public final void close() { } /** - * Initializes this {@link Processor}. + * Initializes this {@link Processor} by calling the {@link Consumer#accept(Object) accept(Object)} method on the + * {@link Consumer} {@linkplain #Processor(Consumer, Runnable) supplied at construction time} with the supplied {@link + * ProcessingEnvironment}, and then blocking until another thread invokes the {@link #close()} + * method. * * @param pe a {@link ProcessingEnvironment}; must not be {@code null} * * @deprecated This method should be called only by a Java compiler in accordance with annotation processing - * contracts. + * contracts. All other usage will result in undefined behavior. */ @Deprecated // to be called only by a Java compiler in accordance with annotation processing contracts @Override // Processor; @@ -108,6 +139,21 @@ public final void init(final ProcessingEnvironment pe) { } } + /** + * Returns an {@linkplain List#of() empty, immutable, determinate List} when invoked, regardless of + * arguments. + * + * @param element ignored; may be {@code null} + * + * @param annotation ignored; may be {@code null} + * + * @param member ignored; may be {@code null} + * + * @param userText ignored; may be {@code null} + * + * @return an {@linkplain List#of() empty, immutable, determinate List} when invoked, regardless of + * arguments + */ @Override // Processor public final Iterable getCompletions(final Element element, final AnnotationMirror annotation, @@ -116,21 +162,43 @@ public final Iterable getCompletions(final Element element return List.of(); } + /** + * Returns an {@linkplain Set#of() empty, immutable, determinate Set} when invoked. + * + * @return an {@linkplain Set#of() empty, immutable, determinate Set} when invoked + */ @Override // Processor public final Set getSupportedAnnotationTypes() { return Set.of(); } + /** + * Returns an {@linkplain Set#of() empty, immutable, determinate Set} when invoked. + * + * @return an {@linkplain Set#of() empty, immutable, determinate Set} when invoked + */ @Override // Processor public final Set getSupportedOptions() { return Set.of(); } + /** + * Returns the return value of an invocation of the {@link SourceVersion#latestSupported()} method when invoked. + * + * @return the return value of an invocation of the {@link SourceVersion#latestSupported()} method when invoked + */ @Override // Processor public final SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } + /** + * Returns {@code false} when invoked, regardless of arguments. + * + * @param annotations ignored; may be {@code null} + * + * @param roundEnvironment ignored; may be {@code null} + */ @Override // Processor public final boolean process(final Set annotations, final RoundEnvironment roundEnvironment) { return false; diff --git a/src/main/java/org/microbean/construct/element/AnnotationMirrors.java b/src/main/java/org/microbean/construct/element/AnnotationMirrors.java new file mode 100644 index 0000000..8ac21b1 --- /dev/null +++ b/src/main/java/org/microbean/construct/element/AnnotationMirrors.java @@ -0,0 +1,198 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2026 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.construct.element; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SequencedMap; + +import java.util.function.Predicate; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.QualifiedNameable; + +import static java.util.Collections.unmodifiableSequencedMap; + +import static java.util.LinkedHashMap.newLinkedHashMap; + +import static javax.lang.model.util.ElementFilter.methodsIn; + +/** + * A utility class for working with annotations as represented by {@link AnnotationMirror}s, {@link ExecutableElement}s, + * and {@link AnnotationValue}s. + * + * @author Laird Nelson + */ +public final class AnnotationMirrors { + + private static final SequencedMap EMPTY_MAP = unmodifiableSequencedMap(newLinkedHashMap(0)); + + private static final SameAnnotationValueVisitor sameAnnotationValueVisitor = new SameAnnotationValueVisitor(); + + private AnnotationMirrors() { + super(); + } + + /** + * For the supplied {@link AnnotationMirror}, returns an immutable, determinate {@link Map} of {@link AnnotationValue} + * instances indexed by its {@link ExecutableElement}s to which they apply. + * + *

Each {@link ExecutableElement} represents an annotation element and meets the + * requirements of such an element.

+ * + *

Each {@link AnnotationValue} represents the value of an annotation element and meets the requirements for + * annotation values.

+ * + *

This method is a more capable, better-typed replacement of the {@link + * javax.lang.model.util.Elements#getElementValuesWithDefaults(AnnotationMirror)} method, and should be preferred.

+ * + * @param a an {@link AnnotationMirror}; may be {@code null} in which case an empty, immutable, determinate {@link + * Map} will be returned + * + * @return an immutable, determinate {@link Map} of {@link AnnotationValue} instances indexed by {@link + * ExecutableElement}s + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, section + * 9.6.1 + * + * @see javax.lang.model.util.Elements#getElementValuesWithDefaults(AnnotationMirror) + */ + public static final SequencedMap allAnnotationValues(final AnnotationMirror a) { + if (a == null) { + return emptySequencedMap(); + } + final Collection elements = methodsIn(a.getAnnotationType().asElement().getEnclosedElements()); + if (elements.isEmpty()) { + return emptySequencedMap(); + } + final SequencedMap m = newLinkedHashMap(elements.size()); + final Map explicitValues = a.getElementValues(); + for (final ExecutableElement ee : elements) { + // You're going to want to use getOrDefault() here. Go ahead and try but you'll run into typing issues. + m.put(ee, explicitValues.containsKey(ee) ? explicitValues.get(ee) : ee.getDefaultValue()); + } + return m.isEmpty() ? emptySequencedMap() : unmodifiableSequencedMap(m); + } + + /** + * Determines whether the two {@link AnnotationMirror}s represent the same (otherwise opaque) annotation. + * + * @param am0 an {@link AnnotationMirror}; may be {@code null} + * + * @param am1 an {@link AnnotationMirror}; may be {@code null} + * + * @return {@code true} if the supplied {@link AnnotationMirror}s represent the same (otherwise opaque) annotation; + * {@code false} otherwise + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) + */ + public static final boolean sameAnnotation(final AnnotationMirror am0, final AnnotationMirror am1) { + return sameAnnotation(am0, am1, x -> true); + } + + /** + * Determines whether the two {@link AnnotationMirror}s represent the same (underlying, otherwise opaque) annotation. + * + * @param am0 an {@link AnnotationMirror}; may be {@code null} + * + * @param am1 an {@link AnnotationMirror}; may be {@code null} + * + * @param p a {@link Predicate} that returns {@code true} if a given {@link ExecutableElement}, representing an + * annotation element, is to be included in the computation; may be {@code null} in which case it is as if + * {@code ()-> true} were supplied instead + * + * @return {@code true} if the supplied {@link AnnotationMirror}s represent the same (underlying, otherwise opaque) + * annotation; {@code false} otherwise + * + * @see SameAnnotationValueVisitor + * + * @see #allAnnotationValues(AnnotationMirror) + */ + public static final boolean sameAnnotation(final AnnotationMirror am0, + final AnnotationMirror am1, + Predicate p) { + if (am0 == am1) { + return true; + } else if (am0 == null || am1 == null) { + return false; + } + if (p == null) { + p = x -> true; + } + final QualifiedNameable qn0 = (QualifiedNameable)am0.getAnnotationType().asElement(); + final QualifiedNameable qn1 = (QualifiedNameable)am1.getAnnotationType().asElement(); + if (qn0 != qn1 && !qn0.getQualifiedName().contentEquals(qn1.getQualifiedName())) { + return false; + } + final SequencedMap m0 = allAnnotationValues(am0); + final SequencedMap m1 = allAnnotationValues(am1); + if (m0.size() != m1.size()) { + return false; + } + final Iterator> i0 = m0.entrySet().iterator(); + final Iterator> i1 = m1.entrySet().iterator(); + while (i0.hasNext()) { + final Entry e0 = i0.next(); + final Entry e1 = i1.next(); + final ExecutableElement ee0 = e0.getKey(); + if (p.test(ee0) && + (!e0.getKey().getSimpleName().contentEquals(e1.getKey().getSimpleName()) || + !sameAnnotationValueVisitor.visit(e0.getValue(), e1.getValue().getValue()))) { + return false; + } + } + return !i1.hasNext(); + } + + /** + * A convenience method that returns a value for an annotation element named by the supplied {@link CharSequence} and + * logically owned by the supplied {@link AnnotationMirror}, or {@code null} if no such value exists. + * + * @param am an {@link AnnotationMirror}; must not be {@code null} + * + * @param name a {@link CharSequence}; may be {@code null} in which case {@code null} will be returned + * + * @return the result of invoking {@link AnnotationValue#getValue() getValue()} on an {@link AnnotationValue}, or + * {@code null} + * + * @exception NullPointerException if {@code am} is {@code null} + * + * @see AnnotationValue + * + * @see #allAnnotationValues(AnnotationMirror) + */ + public static final Object get(final AnnotationMirror am, final CharSequence name) { + if (name == null) { + return null; + } + for (final Entry e : allAnnotationValues(am).entrySet()) { + if (e.getKey().getSimpleName().contentEquals(name)) { + final AnnotationValue av = e.getValue(); + return av == null ? null : av.getValue(); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static final SequencedMap emptySequencedMap() { + return (SequencedMap)EMPTY_MAP; + } + +} diff --git a/src/main/java/org/microbean/construct/element/SameAnnotationValueVisitor.java b/src/main/java/org/microbean/construct/element/SameAnnotationValueVisitor.java new file mode 100644 index 0000000..73c2266 --- /dev/null +++ b/src/main/java/org/microbean/construct/element/SameAnnotationValueVisitor.java @@ -0,0 +1,262 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2026 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.construct.element; + +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; + +import javax.lang.model.util.AbstractAnnotationValueVisitor14; + +import static javax.lang.model.element.ElementKind.ENUM; +import static javax.lang.model.element.ElementKind.ENUM_CONSTANT; + +import static javax.lang.model.type.TypeKind.ARRAY; +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.VOID; + +import static org.microbean.construct.element.AnnotationMirrors.allAnnotationValues; + +/** + * An {@link AbstractAnnotationValueVisitor14} that determines if the otherwise opaque values {@linkplain + * AnnotationValue#getValue() represented} by two {@link AnnotationValue} implementations are to be considered the + * same. + * + *

Unlike some other annotation-processing-related facilities, the relation represented by this {@link + * SameAnnotationValueVisitor} does not require that the values being logically compared originate from {@link + * AnnotationValue} instances from the same vendor or toolkit.

+ * + *

The second argument passed to {@link #visit(AnnotationValue, Object)} is expected to be either {@code null}, an + * {@link AnnotationValue}, or the result of an invocation of an {@link AnnotationValue}'s {@link + * AnnotationValue#getValue() getValue()} method.

+ * + *

Any two {@link TypeElement}s encountered during traversal are considered equal if their {@linkplain + * TypeElement#getQualifiedName() qualified names} have {@linkplain + * javax.lang.model.element.Name#contentEquals(CharSequence) equal contents}.

+ * + *

Any two {@link VariableElement}s representing enum constants encountered during traversal are considered equal if + * their {@linkplain VariableElement#getSimpleName() simple names} have {@linkplain + * javax.lang.model.element.Name#contentEquals(CharSequence) equal contents}.

+ * + * @author Laird Nelson + * + * @see AnnotationValue#accept(javax.lang.model.element.AnnotationValueVisitor, Object) + * + * @see AnnotationMirrors#allAnnotationValues(AnnotationMirror) + */ +public final class SameAnnotationValueVisitor extends AbstractAnnotationValueVisitor14 { + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link SameAnnotationValueVisitor}. + */ + public SameAnnotationValueVisitor() { + super(); + } + + + /* + * Instance methods. + */ + + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitAnnotation(final AnnotationMirror am0, final Object v1) { + return am0 == v1 || am0 != null && switch (v1) { + case null -> false; + case AnnotationValue av1 -> this.visitAnnotation(am0, av1.getValue()); + case AnnotationMirror am1 -> { + final Iterator> i0 = allAnnotationValues(am0).entrySet().iterator(); + final Iterator> i1 = allAnnotationValues(am1).entrySet().iterator(); + while (i0.hasNext()) { + if (!i1.hasNext()) { + yield false; + } + final Entry e0 = i0.next(); + final Entry e1 = i1.next(); + if (!e0.getKey().getSimpleName().contentEquals(e1.getKey().getSimpleName()) || + !this.visit(e0.getValue(), e1.getValue().getValue())) { + yield false; + } + } + yield i1.hasNext(); + } + default -> false; + }; + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitArray(final List l0, final Object v1) { + return l0 == v1 || l0 != null && switch (v1) { + case null -> false; + case AnnotationValue av1 -> this.visitArray(l0, av1.getValue()); + case List l1 -> { + final int size = l0.size(); + if (size != l1.size()) { + yield false; + } + for (int i = 0; i < size; i++) { + if (l1.get(i) instanceof AnnotationValue av1 && this.visit(av1, l0.get(i).getValue())) { + continue; + } + yield false; + } + yield true; + } + default -> false; + }; + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitBoolean(final boolean b0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitBoolean(b0, av1.getValue()) : Boolean.valueOf(b0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitByte(final byte b0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitByte(b0, av1.getValue()) : Byte.valueOf(b0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitChar(final char c0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitChar(c0, av1.getValue()) : Character.valueOf(c0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitDouble(final double d0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitDouble(d0, av1.getValue()) : Double.valueOf(d0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitEnumConstant(final VariableElement ve0, final Object v1) { + return ve0 == v1 || ve0!= null && switch (v1) { + case null -> false; + case AnnotationValue av1 -> this.visitEnumConstant(ve0, av1.getValue()); + case VariableElement ve1 when ve0.getKind() == ENUM_CONSTANT && ve1.getKind() == ENUM_CONSTANT -> { + final TypeElement te0 = (TypeElement)ve0.getEnclosingElement(); + final TypeElement te1 = (TypeElement)ve1.getEnclosingElement(); + yield switch (te0.getKind()) { + case ENUM -> te1.getKind() == ENUM && ve0.getSimpleName().contentEquals(ve1.getSimpleName()) && te0.getQualifiedName().contentEquals(te1.getQualifiedName()); + default -> false; + }; + } + default -> false; + }; + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitFloat(final float f0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitFloat(f0, av1.getValue()) : Float.valueOf(f0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitInt(final int i0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitInt(i0, av1.getValue()) : Integer.valueOf(i0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitLong(final long l0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitLong(l0, av1.getValue()) : Long.valueOf(l0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitShort(final short s0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitShort(s0, av1.getValue()) : Short.valueOf(s0).equals(v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitString(final String s0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitString(s0, av1.getValue()) : Objects.equals(s0, v1); + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitType(final TypeMirror t0, final Object v1) { + return t0 == v1 || v1 != null && switch (t0) { + case null -> false; + case AnnotationValue av1 -> this.visitType(t0, av1.getValue()); + case ArrayType a0 when a0.getKind() == ARRAY -> this.visitArrayType(a0, v1); // e.g. Object[].class + case DeclaredType dt0 when dt0.getKind() == DECLARED -> this.visitDeclaredType(dt0, v1); // e.g. Foo.class + case PrimitiveType p0 when p0.getKind().isPrimitive() -> this.visitPrimitiveType(p0, v1); // e.g. int.class + case NoType n0 when n0.getKind() == VOID -> this.visitNoType(n0, v1); // e.g. void.class + default -> t0.equals(v1); + }; + } + + @Override // AbstractAnnotationValueVisitor14 + public final Boolean visitUnknown(final AnnotationValue av0, final Object v1) { + return v1 instanceof AnnotationValue av1 ? this.visitUnknown(av0, av1.getValue()) : Objects.equals(av0, v1); + } + + + /* + * Private instance methods. + */ + + + private final Boolean visitArrayType(final ArrayType a0, final Object v1) { + assert a0.getKind() == ARRAY; + assert !(v1 instanceof AnnotationValue); + return switch (v1) { + case ArrayType a1 when a1.getKind() == ARRAY -> this.visitType(a0.getComponentType(), a1.getComponentType()); + default -> false; + }; + } + + private final Boolean visitDeclaredType(final DeclaredType dt0, final Object v1) { + assert dt0.getKind() == DECLARED; + assert !(v1 instanceof AnnotationValue); + return switch (v1) { + case DeclaredType dt1 when dt1.getKind() == DECLARED -> ((QualifiedNameable)dt0.asElement()).getQualifiedName().contentEquals((((QualifiedNameable)dt1.asElement()).getQualifiedName())); + default -> false; + }; + } + + private final Boolean visitNoType(final NoType n0, final Object v1) { + assert n0.getKind() == VOID; + assert !(v1 instanceof AnnotationValue); + return switch (v1) { + case NoType n1 -> n1.getKind() == VOID; + default -> false; + }; + } + + private final Boolean visitPrimitiveType(final PrimitiveType p0, final Object v1) { + assert p0.getKind().isPrimitive(); + assert !(v1 instanceof AnnotationValue); + return switch (v1) { + case PrimitiveType p1 -> p1.getKind() == p0.getKind(); + default -> false; + }; + } + +} diff --git a/src/main/java/org/microbean/construct/element/StringName.java b/src/main/java/org/microbean/construct/element/StringName.java index 0091bc7..5af677d 100644 --- a/src/main/java/org/microbean/construct/element/StringName.java +++ b/src/main/java/org/microbean/construct/element/StringName.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2024–2025 microBean™. + * Copyright © 2024–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -30,6 +30,8 @@ import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.util.Objects.requireNonNull; + /** * A {@link Name} implementation based on {@link String}s. * @@ -64,7 +66,7 @@ public StringName(final CharSequence value, final PrimordialDomain domain) { // We deliberately route even String-typed values through PrimordialDomain#toString(CharSequence) in case the PrimordialDomain wishes to // cache the intermediate Name. this(switch (value) { - case StringName sn -> sn.value(); + case StringName sn -> sn.value; default -> domain.toString(value); }, domain); } @@ -79,23 +81,23 @@ public StringName(final CharSequence value, final PrimordialDomain domain) { * @exception NullPointerException if either argument is {@code null} */ public StringName { - Objects.requireNonNull(value, "value"); - Objects.requireNonNull(domain, "domain"); + requireNonNull(value, "value"); + requireNonNull(domain, "domain"); } @Override // Name (CharSequence) public final char charAt(final int index) { - return this.value().charAt(index); + return this.value.charAt(index); } @Override // Name (CharSequence) public final IntStream chars() { - return this.value().chars(); + return this.value.chars(); } @Override // Name (CharSequence) public final IntStream codePoints() { - return this.value().codePoints(); + return this.value.codePoints(); } @Override // Name @@ -103,10 +105,10 @@ public final IntStream codePoints() { public final boolean contentEquals(final CharSequence cs) { return this == cs || switch (cs) { case null -> false; - case String s -> this.value().contentEquals(s); - case StringName sn -> this.value().contentEquals(sn.value()); - case Name n -> this.value().contentEquals(this.domain().toString(n)); - default -> this.value().contentEquals(cs.toString()); + case String s -> this.value.contentEquals(s); + case StringName sn -> this.value.contentEquals(sn.value); + case Name n -> this.value.contentEquals(this.domain().toString(n)); + default -> this.value.contentEquals(cs.toString()); }; } @@ -117,7 +119,7 @@ public final Optional describeConstable() { MethodHandleDesc.ofConstructor(ClassDesc.of(this.getClass().getName()), ClassDesc.of(CharSequence.class.getName()), ClassDesc.of(PrimordialDomain.class.getName())), - this.value(), + this.value, domainDesc)); } @@ -125,34 +127,34 @@ public final Optional describeConstable() { public final boolean equals(final Object other) { return this == other || switch (other) { case null -> false; - case StringName sn when this.getClass() == sn.getClass() -> Objects.equals(this.value(), sn.value()); + case StringName sn when this.getClass() == sn.getClass() -> Objects.equals(this.value, sn.value); default -> false; }; } @Override // Record public final int hashCode() { - return this.value().hashCode(); + return this.value.hashCode(); } @Override // Name (CharSequence) public final boolean isEmpty() { - return this.value().isEmpty(); + return this.value.isEmpty(); } @Override // Name (CharSequence) public final int length() { - return this.value().length(); + return this.value.length(); } @Override // Name (CharSequence) public final CharSequence subSequence(final int start, final int end) { - return this.value().subSequence(start, end); + return this.value.subSequence(start, end); } @Override // Name (CharSequence) public final String toString() { - return this.value(); + return this.value; } @@ -163,9 +165,9 @@ public final String toString() { /** * Returns a {@link StringName} whose {@link #value()} method will return a {@link String} {@linkplain - * String#equals(Object) equal to} the {@linkplain PrimordialDomain#toString(CharSequence) String conversion of} - * the supplied {@link CharSequence}, and whose {@link #domain()} method will return a {@link PrimordialDomain} {@linkplain - * #equals(Object) equal to} the supplied {@link PrimordialDomain}. + * String#equals(Object) equal to} the {@linkplain PrimordialDomain#toString(CharSequence) String + * conversion of} the supplied {@link CharSequence}, and whose {@link #domain()} method will return a {@link + * PrimordialDomain} {@linkplain #equals(Object) equal to} the supplied {@link PrimordialDomain}. * * @param cs a {@link CharSequence}; must not be {@code null} * diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java index e0fc4a3..e430e44 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java +++ b/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2025 microBean™. + * Copyright © 2025–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -13,66 +13,90 @@ */ package org.microbean.construct.element; -import java.lang.annotation.Annotation; - import java.lang.constant.ClassDesc; import java.lang.constant.Constable; import java.lang.constant.ConstantDesc; import java.lang.constant.DynamicConstantDesc; -import java.lang.constant.MethodTypeDesc; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; -import java.util.function.Function; - import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.AnnotationValueVisitor; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; import org.microbean.construct.constant.Constables; import static java.lang.constant.ConstantDescs.BSM_INVOKE; import static java.lang.constant.ConstantDescs.CD_Map; -import static java.lang.constant.ConstantDescs.CD_Object; -import static java.lang.constant.ConstantDescs.NULL; - -import static java.lang.constant.DirectMethodHandleDesc.Kind.INTERFACE_STATIC; import static java.lang.constant.MethodHandleDesc.ofConstructor; -import static java.util.Arrays.fill; - import static java.util.Collections.unmodifiableMap; import static java.util.LinkedHashMap.newLinkedHashMap; -import static java.util.Objects.requireNonNull; - import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; -import static javax.lang.model.element.ElementKind.METHOD; + +import static javax.lang.model.util.ElementFilter.methodsIn; /** * An experimental {@link AnnotationMirror} implementation that is partially or wholly synthetic. * + *

It is possible to create {@link SyntheticAnnotationMirror} instances representing annotations that a Java compiler + * will not produce. For example, annotations cannot refer to each + * other, directly or indirectly, but two {@link SyntheticAnnotationMirror}s may do so.

+ * * @author Laird Nelson + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, section + * 9.6.1 */ public final class SyntheticAnnotationMirror implements AnnotationMirror, Constable { + + /* + * Instance fields. + */ + + private final TypeElement annotationTypeElement; private final Map elementValues; + + /* + * Constructors. + */ + + + /** + * Creates a new {@link SyntheticAnnotationMirror}. + * + * @param annotationTypeElement a {@link TypeElement} representing an annotation type; must not be {@code null}; must + * return {@link javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from its {@link + * Element#getKind() getKind()} method; {@link SyntheticAnnotationTypeElement} implementations are strongly preferred + * + * @exception NullPointerException if {@code annotationTypeElement} is {@code null} + * + * @exception IllegalArgumentException if {@code annotationTypeElement} does not return {@link + * javax.lang.model.element.ElementKind#ANNOTATION_TYPE ANNOTATION_TYPE} from an invocation of its {@link + * Element#getKind() getKind()} method, or if {@code values} has more entries in it than {@code annotationTypeElement} + * has {@linkplain Element#getEnclosedElements() anotation elements} + * + * @see #SyntheticAnnotationMirror(TypeElement, Map) + */ + public SyntheticAnnotationMirror(final TypeElement annotationTypeElement) { + this(annotationTypeElement, Map.of()); + } + /** * Creates a new {@link SyntheticAnnotationMirror}. * @@ -98,18 +122,24 @@ public SyntheticAnnotationMirror(final TypeElement annotationTypeElement, } this.annotationTypeElement = annotationTypeElement; final LinkedHashMap m = newLinkedHashMap(values.size()); - for (final Element e : annotationTypeElement.getEnclosedElements()) { - if (e.getKind() == METHOD) { - final Object value = values.get(e.getSimpleName().toString()); - if (value != null) { - m.put((ExecutableElement)e, value instanceof AnnotationValue av ? av : new SyntheticAnnotationValue(value)); + final List methods = methodsIn(annotationTypeElement.getEnclosedElements()); + for (final ExecutableElement e : methods) { + final Object value = values.get(e.getSimpleName().toString()); // default value deliberately not included + if (value == null) { + if (e.getDefaultValue() == null) { + // There has to be a value somewhere, or annotationTypeElement or values is illegal + throw new IllegalArgumentException("annotationTypeElement: " + annotationTypeElement + "; values: " + values); } + // Default values are excluded from the map on purpose, following the contract of + // AnnotationValue#getElementValues(). + } else { + m.put(e, value instanceof AnnotationValue av ? av : new SyntheticAnnotationValue(value)); } } if (values.size() > m.size()) { throw new IllegalArgumentException("values: " + values); } - this.elementValues = unmodifiableMap(m); + this.elementValues = m.isEmpty() ? Map.of() : unmodifiableMap(m); } @@ -117,6 +147,7 @@ public SyntheticAnnotationMirror(final TypeElement annotationTypeElement, * Instance methods. */ + @Override // Constable public final Optional describeConstable() { return this.annotationTypeElement instanceof Constable c ? c.describeConstable() : Optional.empty() @@ -130,7 +161,6 @@ public final Optional describeConstable() { elementDesc, valuesDesc))); } - @Override // AnnotationMirror public final DeclaredType getAnnotationType() { @@ -142,133 +172,11 @@ public final DeclaredType getAnnotationType() { return this.elementValues; } - /** - * An experimental {@link AnnotationValue} implementation that is partially or wholly synthetic. - * - * @author Laird Nelson - */ - public static final class SyntheticAnnotationValue implements AnnotationValue, Constable { - - // Will be one of: - // - // * AnnotationMirror - // * List - // * TypeMirror - // * VariableElement (ENUM_CONSTANT) - // * Boolean - // * Byte - // * Character - // * Double - // * Float - // * Integer - // * Long - // * Short - // * String - private final Object value; - - /** - * Creates a new {@link SyntheticAnnotationValue}. - * - * @param value the value; must not be {@code null}; must be a legal {@link AnnotationValue} type - * - * @exception NullPointerException if {@code value} is {@code null} - * - * @exception IllegalArgumentException if {@code value} is not a legal {@link AnnotationValue} type - * - * @see AnnotationValue - */ - public SyntheticAnnotationValue(final Object value) { - super(); - this.value = value(value); - } - @Override // AnnotationValue - @SuppressWarnings("unchecked") - public final R accept(final AnnotationValueVisitor v, final P p) { - return switch (this.getValue()) { - case null -> v.visitUnknown(this, p); // ...or AssertionError? - case AnnotationMirror a -> v.visitAnnotation(a, p); - case List l -> v.visitArray((List)l, p); - case TypeMirror t -> v.visitType(t, p); - case VariableElement e -> v.visitEnumConstant(e, p); - case Boolean b -> v.visitBoolean(b, p); - case Byte b -> v.visitByte(b, p); - case Character c -> v.visitChar(c, p); - case Double d -> v.visitDouble(d, p); - case Float f -> v.visitFloat(f, p); - case Integer i -> v.visitInt(i, p); - case Long l -> v.visitLong(l, p); - case Short s -> v.visitShort(s, p); - case String s -> v.visitString(s, p); - default -> v.visitUnknown(this, p); - }; - } - - @Override // Constable - public final Optional describeConstable() { - return this.value instanceof Constable c ? c.describeConstable() : Optional.empty() - .map(valueDesc -> DynamicConstantDesc.of(BSM_INVOKE, - ofConstructor(ClassDesc.of(this.getClass().getName()), - CD_Object), - valueDesc)); - } - - @Override // Object - public final boolean equals(final Object other) { - return this == other || switch (other) { - case null -> false; - case SyntheticAnnotationValue sav when this.getClass() == sav.getClass() -> this.value.equals(sav.value); - default -> false; - }; - } - - @Override // AnnotationValue - public final Object getValue() { - return this.value; - } - - @Override // Object - public final int hashCode() { - return this.value.hashCode(); - } - - @Override // Object - public final String toString() { - return this.value.toString(); - } - - private static final Object value(final Object value) { - return switch (value) { - case null -> throw new NullPointerException("value"); - - case AnnotationValue av -> av.getValue(); // not part of the spec; just good hygiene - case List l -> l.stream().map(SyntheticAnnotationValue::new).toList(); - - case TypeMirror t -> switch (t.getKind()) { - case BOOLEAN, BYTE, CHAR, DECLARED, DOUBLE, FLOAT, INT, LONG, SHORT, VOID /* I think? */ -> t; - default -> throw new IllegalArgumentException("value: " + value); - }; - - case VariableElement e -> switch (e.getKind()) { - case ENUM_CONSTANT -> e; - default -> throw new IllegalArgumentException("value: " + value); - }; - - case AnnotationMirror a -> a; - case Boolean b -> b; - case Byte b -> b; - case Character c -> c; - case Double d -> d; - case Float f -> f; - case Integer i -> i; - case Long l -> l; - case Short s -> s; - case String s -> s; - default -> throw new IllegalArgumentException("value: " + value); - }; - } + /* + * Static methods. + */ - } private static final Optional describeAnnotationValue(final Object v) { return switch (v) { diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java index 4a94e5f..2dff366 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java +++ b/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2025 microBean™. + * Copyright © 2025–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -36,6 +36,7 @@ import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; @@ -48,7 +49,6 @@ import static java.util.Objects.requireNonNull; import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; -import static javax.lang.model.element.ElementKind.CLASS; import static javax.lang.model.element.ElementKind.ENUM; import static javax.lang.model.element.Modifier.ABSTRACT; @@ -66,18 +66,100 @@ */ public final class SyntheticAnnotationTypeElement implements TypeElement { - private static final Set modifiers = Set.of(ABSTRACT); + + /* + * Static fields. + */ + + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private static final Set ABSTRACT_ONLY_MODIFIERS = Set.of(ABSTRACT); + + + /* + * Instance fields. + */ + private final List annotationMirrors; private final SyntheticName fqn; + private final SyntheticName sn; + private final Type type; private final List elements; + + /* + * Constructors. + */ + + + /** + * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link + * SyntheticAnnotationMirror} instances. + * + * @param fullyQualifiedName the fully qualified name of the synthetic element; must conform to Java classname + * restrictions; must not be {@code null} + * + * @exception NullPointerException if {@code fullyQualifiedName} is {@code null} + * + * @exception IllegalArgumentException if {@code fullyQualifiedName} does not conform to Java classname restrictions + * + * @see #SyntheticAnnotationTypeElement(List, SyntheticName, List) + */ + public SyntheticAnnotationTypeElement(final SyntheticName fullyQualifiedName) { + this(List.of(), fullyQualifiedName, List.of()); + } + + /** + * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link + * SyntheticAnnotationMirror} instances. + * + * @param fullyQualifiedName the fully qualified name of the synthetic element; must conform to Java classname + * restrictions; must not be {@code null} + * + * @param elements a {@link List} of {@link SyntheticAnnotationElement}s modeling the annotation elements; must not be + * {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if {@code fullyQualifiedName} does not conform to Java classname restrictions + * + * @see #SyntheticAnnotationTypeElement(List, SyntheticName, List) + */ + public SyntheticAnnotationTypeElement(final SyntheticName fullyQualifiedName, + final List elements) { + this(List.of(), fullyQualifiedName, elements); + } + + /** + * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link + * SyntheticAnnotationMirror} instances. + * + * @param annotationMirrors a {@link List} of {@link AnnotationMirror}s modeling the annotations this element has; + * must not be {@code null} + * + * @param fullyQualifiedName the fully qualified name of the synthetic element; must conform to Java classname + * restrictions; must not be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if {@code fullyQualifiedName} does not conform to Java classname restrictions + * + * @see #SyntheticAnnotationTypeElement(List, SyntheticName, List) + */ + public SyntheticAnnotationTypeElement(final List annotationMirrors, + final SyntheticName fullyQualifiedName) { + this(annotationMirrors, fullyQualifiedName, List.of()); + } + /** - * Creates a new {@link SyntheticAnnotationTypeElement}. + * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link + * SyntheticAnnotationMirror} instances. * * @param annotationMirrors a {@link List} of {@link AnnotationMirror}s modeling the annotations this element has; * must not be {@code null} @@ -97,114 +179,252 @@ public final class SyntheticAnnotationTypeElement implements TypeElement { * @see SyntheticAnnotationMirror */ public SyntheticAnnotationTypeElement(final List annotationMirrors, - final String fullyQualifiedName, + final SyntheticName fullyQualifiedName, final List elements) { super(); this.annotationMirrors = List.copyOf(annotationMirrors); - this.fqn = SyntheticName.of(fullyQualifiedName); + final String fqn = fullyQualifiedName.toString(); + final int i = fqn.lastIndexOf('.'); + this.sn = i >= 0 ? new SyntheticName(fqn.substring(i + 1)) : fullyQualifiedName; + this.fqn = fullyQualifiedName; this.type = new Type(); - final List elements0 = new ArrayList<>(elements.size()); - for (final SyntheticAnnotationElement e : elements) { - elements0.add(new InternalAnnotationElement(e.annotationMirrors(), e.type(), e.name())); + if (elements.isEmpty()) { + this.elements = List.of(); + } else { + final List elements0 = new ArrayList<>(elements.size()); + for (final SyntheticAnnotationElement e : elements) { + elements0.add(new InternalAnnotationElement(e.annotationMirrors(), e.type(), e.name(), e.defaultValue())); + } + this.elements = unmodifiableList(elements0); } - this.elements = unmodifiableList(elements0); } - @Override // Element + + /* + * Instance methods. + */ + + + @Override // TypeElement (Element) public final R accept(final ElementVisitor v, final P p) { return v.visitType(this, p); } - @Override // Element + @Override // TypeElement (Element) public final TypeMirror asType() { return this.type; } - @Override // AnnotatedConstruct + @Override // TypeElement (AnnotatedConstruct) public final List getAnnotationMirrors() { return this.annotationMirrors; } - @Override // AnnotatedConstruct + @Override // TypeElement (AnnotatedConstruct) public final A getAnnotation(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return null; // deliberate } - @Override // AnnotatedConstruct + @Override // TypeElement (AnnotatedConstruct) + @SuppressWarnings("unchecked") public final A[] getAnnotationsByType(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return (A[])EMPTY_ANNOTATION_ARRAY; // deliberate } - @Override + @Override // TypeElement (Element) public final List getEnclosedElements() { return this.elements; } - @Override + @Override // TypeElement (Element) public final Element getEnclosingElement() { return null; // really should be a package but we're synthetic } - @Override + @Override // TypeElement public final List getInterfaces() { return List.of(); } - @Override + @Override // TypeElement (Element) public final Set getModifiers() { - return modifiers; + return ABSTRACT_ONLY_MODIFIERS; } - @Override + @Override // TypeElement (Element) public final ElementKind getKind() { return ElementKind.ANNOTATION_TYPE; } - @Override + @Override // TypeElement public final NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; } - @Override - public final Name getQualifiedName() { + @Override // TypeElement (QualifiedNameable) + public final SyntheticName getQualifiedName() { return this.fqn; } - @Override - public final Name getSimpleName() { - final String fqn = this.getQualifiedName().toString(); - final int i = fqn.lastIndexOf('.'); - return i >= 0 ? SyntheticName.of(fqn.substring(i + 1)) : this.getQualifiedName(); + @Override // TypeElement (Element) + public final SyntheticName getSimpleName() { + return this.sn; } - @Override + @Override // TypeElement public final TypeMirror getSuperclass() { return NoneType.of(); } - @Override + @Override // TypeElement public final List getTypeParameters() { return List.of(); } - @Override + @Override // TypeElement (Element) public final String toString() { - return "@" + this.getQualifiedName().toString(); // TODO: robustify + return this.getQualifiedName().toString(); // TODO: robustify + } + + + /* + * Static methods. + */ + + + // See https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 + private static final TypeMirror validateScalarType(final TypeMirror type) { + return switch (type) { + case null -> throw new NullPointerException("t"); + case PrimitiveType t when t.getKind().isPrimitive() -> t; + case DeclaredType t when t.getKind() == DECLARED -> { + final TypeElement te = (TypeElement)t.asElement(); + yield switch (te.getKind()) { + case ANNOTATION_TYPE, ENUM -> t; + default -> { + final Name fqn = te.getQualifiedName(); + if (fqn.contentEquals("java.lang.Class") || fqn.contentEquals("java.lang.String")) { + yield t; + } + throw new IllegalArgumentException("type: " + type); + } + }; + } + default -> throw new IllegalArgumentException("type: " + type); + }; } + + /* + * Inner and nested classes. + */ + + /** * An experimental collection of information out of which a synthetic annotation element may be * fashioned. * * @author Laird Nelson + * + * @see SyntheticAnnotationTypeElement#SyntheticAnnotationTypeElement(List, SyntheticName, List) */ public static final class SyntheticAnnotationElement { + + /* + * Instance fields. + */ + + private final List annotationMirrors; private final TypeMirror type; - private final String name; + private final SyntheticName name; + + private final SyntheticAnnotationValue defaultValue; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link SyntheticAnnotationElement}. + * + * @param type the type of the annotation element; must conform to Java annotation element type restrictions; must + * not be {@code null} + * + * @param name the name of the annotation element; must conform to Java method naming requirements; must not be + * {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if any argument does not conform to its requirements + * + * @see #SyntheticAnnotationElement(List, TypeMirror, SyntheticName, SyntheticAnnotationValue) + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, + * section 9.6.1 + */ + public SyntheticAnnotationElement(final TypeMirror type, // the "return type" + final SyntheticName name) { + this(List.of(), type, name, null); + } + + /** + * Creates a new {@link SyntheticAnnotationElement}. + * + * @param type the type of the annotation element; must conform to Java annotation element type restrictions; must + * not be {@code null} + * + * @param name the name of the annotation element; must conform to Java method naming requirements; must not be + * {@code null} + * + * @param defaultValue a {@link SyntheticAnnotationValue} representing the default value; may be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if any argument does not conform to its requirements + * + * @see #SyntheticAnnotationElement(List, TypeMirror, SyntheticName, SyntheticAnnotationValue) + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, + * section 9.6.1 + */ + public SyntheticAnnotationElement(final TypeMirror type, // the "return type" + final SyntheticName name, + final SyntheticAnnotationValue defaultValue) { + this(List.of(), type, name, defaultValue); + } + + /** + * Creates a new {@link SyntheticAnnotationElement}. + * + * @param annotationMirrors a {@link List} of {@link AnnotationMirror}s modeling the annotations this element has; + * must not be {@code null} + * + * @param type the type of the annotation element; must conform to Java annotation element type restrictions; must + * not be {@code null} + * + * @param name the name of the annotation element; must conform to Java method naming requirements; must not be + * {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if any argument does not conform to its requirements + * + * @see #SyntheticAnnotationElement(List, TypeMirror, SyntheticName, SyntheticAnnotationValue) + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, + * section 9.6.1 + */ + public SyntheticAnnotationElement(final List annotationMirrors, + final TypeMirror type, // the "return type" + final SyntheticName name) { + this(annotationMirrors, type, name, null); + } /** * Creates a new {@link SyntheticAnnotationElement}. @@ -218,19 +438,40 @@ public static final class SyntheticAnnotationElement { * @param name the name of the annotation element; must conform to Java method naming requirements; must not be * {@code null} * + * @param defaultValue a {@link SyntheticAnnotationValue} representing the default value; may be {@code null} + * * @exception NullPointerException if any argument is {@code null} * * @exception IllegalArgumentException if any argument does not conform to its requirements + * + * @spec https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1 Java Language Specification, + * section 9.6.1 */ public SyntheticAnnotationElement(final List annotationMirrors, final TypeMirror type, // the "return type" - final String name) { + final SyntheticName name, + final SyntheticAnnotationValue defaultValue) { super(); this.annotationMirrors = List.copyOf(annotationMirrors); - this.type = requireNonNull(type, "type"); - this.name = requireNonNull(name, "name"); + this.type = switch (type) { + case null -> throw new NullPointerException("type"); + case ArrayType t when t.getKind() == ARRAY -> validateScalarType(t.getComponentType()); + default -> validateScalarType(type); + }; + if (name.equals("getClass") || name.equals("hashCode") || name.equals("toString")) { + // java.lang.Object-declared methods that might otherwise meet annotation element requirements + throw new IllegalArgumentException("name: " + name); + } + this.name = name; + this.defaultValue = defaultValue; } + + /* + * Instance fields. + */ + + final List annotationMirrors() { return this.annotationMirrors; } @@ -242,7 +483,7 @@ public final boolean equals(final Object other) { case SyntheticAnnotationElement sae when this.getClass() == sae.getClass() -> Objects.equals(this.name, sae.name) && Objects.equals(this.type, sae.type) && - Objects.equals(this.annotationMirrors, sae.annotationMirrors); + Objects.equals(this.annotationMirrors, sae.annotationMirrors); // TODO: hmm default -> false; }; } @@ -254,12 +495,12 @@ public final int hashCode() { hashCode = 17 * hashCode + c; c = this.type.hashCode(); hashCode = 17 * hashCode + c; - c = this.annotationMirrors.hashCode(); + c = this.annotationMirrors.hashCode(); // TODO: hmm hashCode = 17 * hashCode + c; return hashCode; } - final String name() { + final SyntheticName name() { return this.name; } @@ -267,6 +508,10 @@ final TypeMirror type() { return this.type; } + final SyntheticAnnotationValue defaultValue() { + return this.defaultValue; + } + } /** @@ -277,30 +522,41 @@ final TypeMirror type() { * * @see SyntheticAnnotationTypeElement */ + // Note: inner class; see asElement() private class Type implements DeclaredType { - private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + /* + * Constructors. + */ + private Type() { super(); } - @Override // TypeMirror + + /* + * Instance fields. + */ + + + @Override // DeclaredType (TypeMirror) public final R accept(final TypeVisitor v, final P p) { return v.visitDeclared(this, p); } - @Override // ExecutableType (AnnotatedConstruct) + @Override // DeclaredType (AnnotatedConstruct) public final A getAnnotation(final Class annotationType) { return null; // deliberate } - @Override // ExecutableType (AnnotatedConstruct) + @Override // DeclaredType (AnnotatedConstruct) public final List getAnnotationMirrors() { return List.of(); // deliberate } - @Override // ExecutableType (AnnotatedConstruct) + @Override // DeclaredType (AnnotatedConstruct) @SuppressWarnings("unchecked") public final A[] getAnnotationsByType(final Class annotationType) { return (A[])EMPTY_ANNOTATION_ARRAY; // deliberate @@ -311,12 +567,12 @@ public final TypeKind getKind() { return TypeKind.DECLARED; } - @Override - public final TypeMirror getEnclosingType() { + @Override // DeclaredType + public final NoneType getEnclosingType() { return NoneType.of(); } - @Override // DeclaredType (TypeMirror) + @Override // DeclaredType public final List getTypeArguments() { return List.of(); } @@ -326,157 +582,180 @@ public final SyntheticAnnotationTypeElement asElement() { return SyntheticAnnotationTypeElement.this; } - @Override + @Override // DeclaredType (TypeMirror) public final String toString() { return this.asElement().toString(); } } + // Note: inner class, built out of SyntheticAnnotationElement instances; see getEnclosingElement() private final class InternalAnnotationElement implements ExecutableElement { + + /* + * Instance fields. + */ + + private final List annotationMirrors; private final Type t; private final SyntheticName name; + private final SyntheticAnnotationValue defaultValue; + + + /* + * Constructors. + */ + + private InternalAnnotationElement(final List annotationMirrors, final TypeMirror type, - final String name) { + final SyntheticName name, + final SyntheticAnnotationValue defaultValue) { super(); this.annotationMirrors = List.copyOf(annotationMirrors); this.t = new Type(type); - this.name = SyntheticName.of(name); + this.name = requireNonNull(name, "name"); + this.defaultValue = defaultValue; } - @Override // Element + + /* + * Instance fields. + */ + + + @Override // ExecutableElement public final R accept(final ElementVisitor v, final P p) { return v.visitExecutable(this, p); } - @Override // Element + @Override // ExecutableElement public final Type asType() { return this.t; } - @Override // AnnotatedConstruct + @Override // ExecutableElement (AnnotatedConstruct) public final List getAnnotationMirrors() { return this.annotationMirrors; } - @Override // AnnotatedConstruct + @Override // ExecutableElement (AnnotatedConstruct) public final A getAnnotation(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return null; // deliberate } - @Override // AnnotatedConstruct + @Override // ExecutableElement (AnnotatedConstruct) + @SuppressWarnings("unchecked") public final A[] getAnnotationsByType(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return (A[])EMPTY_ANNOTATION_ARRAY; // deliberate } - @Override + @Override // ExecutableElement public AnnotationValue getDefaultValue() { - return null; // deliberate + return this.defaultValue; } - @Override + @Override // ExecutableElement (Element) public final List getEnclosedElements() { return List.of(); } - @Override + @Override // ExecutableElement (Element) public final SyntheticAnnotationTypeElement getEnclosingElement() { return SyntheticAnnotationTypeElement.this; } - @Override + @Override // ExecutableElement (Element) public final Set getModifiers() { - throw new UnsupportedOperationException(); + return ABSTRACT_ONLY_MODIFIERS; } - @Override + @Override // ExecutableElement (Element) public final ElementKind getKind() { return ElementKind.METHOD; } - @Override + @Override // ExecutableElement public final List getParameters() { return List.of(); } - @Override + @Override // ExecutableElement public final TypeMirror getReceiverType() { - return asType().getReceiverType(); + return this.asType().getReceiverType(); } - @Override + @Override // ExecutableElement public final TypeMirror getReturnType() { - return asType().getReturnType(); + return this.asType().getReturnType(); } - @Override - public final Name getSimpleName() { + @Override // ExecutableElement (Element) + public final SyntheticName getSimpleName() { return this.name; } - @Override + @Override // ExecutableElement public final List getThrownTypes() { - return asType().getThrownTypes(); + return this.asType().getThrownTypes(); // (should be zero) } - @Override + @Override // ExecutableElement public final List getTypeParameters() { return List.of(); } - @Override + @Override // ExecutableElement public final boolean isDefault() { - return false; + return false; // technically this could be true if java.lang.annotation.Annotation ever gets a default method } - @Override + @Override // ExecutableElement public final boolean isVarArgs() { return false; } + + /* + * Inner and nested classes. + */ + + private static final class Type implements ExecutableType { + + /* + * Instance fields. + */ + + private final TypeMirror type; - private Type(final TypeMirror type) { + + /* + * Constructors. + */ + + + private Type(final TypeMirror type) { // the "return type" super(); - this.type = validate(type); + this.type = switch (type) { + case null -> throw new NullPointerException("type"); + case ArrayType t when t.getKind() == ARRAY -> validateScalarType(t.getComponentType()); + default -> validateScalarType(type); + }; } - private static TypeMirror validate(final TypeMirror type) { - TypeMirror t = type; - TypeKind k = t.getKind(); - if (k == ARRAY && t instanceof ArrayType a) { - t = a.getComponentType(); - k = t.getKind(); - } - if (k.isPrimitive()) { - return type; - } - if (k == DECLARED) { - final TypeElement e = (TypeElement)((DeclaredType)t).asElement(); - switch (e.getKind()) { - case ANNOTATION_TYPE: - case ENUM: - return type; - case CLASS: - final Name fqn = e.getQualifiedName(); - if (fqn.contentEquals("java.lang.String") || fqn.contentEquals("java.lang.Class")) { - return type; - } - break; - default: - break; - } - } - throw new IllegalArgumentException("type: " + type); - } + + /* + * Instance methods. + */ + @Override // TypeMirror public final R accept(final TypeVisitor v, final P p) { @@ -485,7 +764,7 @@ public final R accept(final TypeVisitor v, final P p) { @Override // ExecutableType (AnnotatedConstruct) public final A getAnnotation(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return null; // deliberate } @Override // ExecutableType (AnnotatedConstruct) @@ -494,8 +773,9 @@ public final List getAnnotationMirrors() { } @Override // ExecutableType (AnnotatedConstruct) + @SuppressWarnings("unchecked") public final A[] getAnnotationsByType(final Class annotationType) { - throw new UnsupportedOperationException(); // deliberate + return (A[])EMPTY_ANNOTATION_ARRAY; // deliberate } @Override // ExecutableType (TypeMirror) @@ -509,7 +789,7 @@ public final List getParameterTypes() { } @Override // ExecutableType - public final TypeMirror getReceiverType() { + public final NoneType getReceiverType() { return NoneType.of(); } diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationValue.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationValue.java new file mode 100644 index 0000000..0d2675f --- /dev/null +++ b/src/main/java/org/microbean/construct/element/SyntheticAnnotationValue.java @@ -0,0 +1,212 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2026 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.construct.element; + +import java.lang.constant.ClassDesc; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DynamicConstantDesc; + +import java.util.List; +import java.util.Optional; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.AnnotationValueVisitor; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.TypeMirror; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_Object; + +import static java.lang.constant.MethodHandleDesc.ofConstructor; + +/** + * An experimental {@link AnnotationValue} implementation that is partially or wholly synthetic. + * + * @author Laird Nelson + */ +public final class SyntheticAnnotationValue implements AnnotationValue, Constable { + + + /* + * Instance fields. + */ + + + // Will be one of: + // + // * AnnotationMirror + // * List + // * TypeMirror (DeclaredType, PrimitiveType) + // * VariableElement (ENUM_CONSTANT) + // * Boolean + // * Byte + // * Character + // * Double + // * Float + // * Integer + // * Long + // * Short + // * String + private final Object value; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link SyntheticAnnotationValue}. + * + * @param value a value legal for an {@link AnnotationValue}; must not be {@code null} + * + * @exception NullPointerException if {@code value} is {@code null} + * + * @exception IllegalArgumentException if {@code value} is not legal for an {@link AnnotationValue} + * + * @see AnnotationValue + */ + public SyntheticAnnotationValue(final Object value) { + super(); + this.value = value(value); + } + + + /* + * Instance methods. + */ + + + @Override // AnnotationValue + @SuppressWarnings("unchecked") + public final R accept(final AnnotationValueVisitor v, final P p) { + return switch (this.value) { + case null -> v.visitUnknown(this, p); // ...or AssertionError? + case AnnotationMirror a -> v.visitAnnotation(a, p); + case List l -> v.visitArray((List)l, p); + case TypeMirror t -> v.visitType(t, p); + case VariableElement e -> v.visitEnumConstant(e, p); + case Boolean b -> v.visitBoolean(b, p); + case Byte b -> v.visitByte(b, p); + case Character c -> v.visitChar(c, p); + case Double d -> v.visitDouble(d, p); + case Float f -> v.visitFloat(f, p); + case Integer i -> v.visitInt(i, p); + case Long l -> v.visitLong(l, p); + case Short s -> v.visitShort(s, p); + case String s -> v.visitString(s, p); + default -> v.visitUnknown(this, p); + }; + } + + @Override // Constable + public final Optional describeConstable() { + return this.value instanceof Constable c ? c.describeConstable() : Optional.empty() + .map(valueDesc -> DynamicConstantDesc.of(BSM_INVOKE, + ofConstructor(ClassDesc.of(this.getClass().getName()), + CD_Object), + valueDesc)); + } + + @Override // Object + public final boolean equals(final Object other) { + return this == other || switch (other) { + case null -> false; + case SyntheticAnnotationValue sav when this.getClass() == sav.getClass() -> this.value.equals(sav.value); + default -> false; + }; + } + + @Override // AnnotationValue + public final Object getValue() { + return this.value; + } + + @Override // Object + public final int hashCode() { + return this.value.hashCode(); + } + + @Override // Object + public final String toString() { + return this.value.toString(); + } + + + /* + * Static methods. + */ + + + /** + * Returns a non-{@code null}, determinate {@link SyntheticAnnotationValue} that represents the supplied {@code + * value}. + * + *

If {@code value} is a {@link SyntheticAnnotationValue}, then it is returned unchanged.

+ * + * @param value a value legal for an {@link AnnotationValue}; must not be {@code null} + * + * @return a non-{@code null}, determinate {@link SyntheticAnnotationValue} + * + * @exception NullPointerException if {@code value} is {@code null} + * + * @exception IllegalArgumentException if {@code value} is not legal for an {@link AnnotationValue} + * + * @see AnnotationValue + */ + public static final SyntheticAnnotationValue of(final Object value) { + return switch (value) { + case null -> throw new NullPointerException("value"); + case SyntheticAnnotationValue sav -> sav; + default -> new SyntheticAnnotationValue(value); + }; + } + + private static final Object value(final Object value) { + return switch (value) { + case null -> throw new NullPointerException("value"); + + case AnnotationValue av -> av.getValue(); // not part of the spec; just good hygiene + + case List l -> l.stream().map(SyntheticAnnotationValue::new).toList(); + + case TypeMirror t -> switch (t.getKind()) { + case ARRAY, BOOLEAN, BYTE, CHAR, DECLARED, DOUBLE, FLOAT, INT, LONG, SHORT, VOID -> t; + default -> throw new IllegalArgumentException("value: " + value); + }; + + case VariableElement e -> switch (e.getKind()) { + case ENUM_CONSTANT -> e; + default -> throw new IllegalArgumentException("value: " + value); + }; + + case AnnotationMirror a -> a; + case Boolean b -> b; + case Byte b -> b; + case Character c -> c; + case Double d -> d; + case Float f -> f; + case Integer i -> i; + case Long l -> l; + case Short s -> s; + case String s -> s; + + default -> throw new IllegalArgumentException("value: " + value); + }; + } + +} diff --git a/src/main/java/org/microbean/construct/element/SyntheticName.java b/src/main/java/org/microbean/construct/element/SyntheticName.java index 116132a..98dde45 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticName.java +++ b/src/main/java/org/microbean/construct/element/SyntheticName.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2025 microBean™. + * Copyright © 2025–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -22,9 +22,6 @@ import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - import java.util.stream.IntStream; import javax.lang.model.element.Name; @@ -50,10 +47,20 @@ */ public final class SyntheticName implements Constable, Name { - private static final ConcurrentMap names = new ConcurrentHashMap<>(); + + /* + * Instance fields. + */ + private final String value; + + /* + * Constructors. + */ + + /** * Creates a new {@link SyntheticName}. * @@ -61,11 +68,17 @@ public final class SyntheticName implements Constable, Name { * * @exception NullPointerException if {@code value} is {@code null} */ - private SyntheticName(final String value) { + public SyntheticName(final String value) { super(); this.value = requireNonNull(value, "value"); } + + /* + * Instance methods. + */ + + @Override // Name (CharSequence) public final char charAt(final int index) { return this.value.charAt(index); @@ -131,17 +144,4 @@ public final String toString() { return this.value; } - /** - * Returns a (non-{@code null}) {@link SyntheticName} representing the supplied {@code name}. - * - * @param name a {@link String}; must not be {@code null} - * - * @return a {@link SyntheticName}; never {@code null} - * - * @exception NullPointerException if {@code name} is {@code null} - */ - public static final SyntheticName of(final String name) { - return names.computeIfAbsent(name, SyntheticName::new); - } - } diff --git a/src/main/java/org/microbean/construct/element/UniversalAnnotation.java b/src/main/java/org/microbean/construct/element/UniversalAnnotation.java index 2e9c705..c6304ef 100644 --- a/src/main/java/org/microbean/construct/element/UniversalAnnotation.java +++ b/src/main/java/org/microbean/construct/element/UniversalAnnotation.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2025 microBean™. + * Copyright © 2025–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -26,9 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.function.Supplier; @@ -37,7 +35,6 @@ import javax.lang.model.element.ExecutableElement; import org.microbean.construct.PrimordialDomain; -import org.microbean.construct.UniversalConstruct; import org.microbean.construct.type.UniversalType; diff --git a/src/main/java/org/microbean/construct/element/UniversalAnnotationValue.java b/src/main/java/org/microbean/construct/element/UniversalAnnotationValue.java index 4a66251..23d77a9 100644 --- a/src/main/java/org/microbean/construct/element/UniversalAnnotationValue.java +++ b/src/main/java/org/microbean/construct/element/UniversalAnnotationValue.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2025 microBean™. + * Copyright © 2025–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -23,7 +23,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.AnnotationValueVisitor; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; @@ -44,15 +43,18 @@ public final class UniversalAnnotationValue implements AnnotationValue { - // Eventually this should become a lazy constant/stable value - // volatile not needed - private Supplier delegateSupplier; + /* + * Instance fields. + */ - private final PrimordialDomain domain; - private String s; + private final PrimordialDomain domain; - private Object v; + // Eventually this should become a lazy constant/stable value + // volatile not needed + // Treat as final + // Use delegate() to read; do not reference directly + private Supplier delegateSupplier; /* @@ -81,21 +83,14 @@ public UniversalAnnotationValue(final AnnotationValue delegate, final Primordial // Should trigger symbol completion; see // https://github.com/openjdk/jdk/blob/jdk-25%2B3/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Constants.java#L49; // any invocation of getTag() will do it. - // - // We bother to eagerly cache the value and the string representation because honestly you're going to call - // those methods anyway, probably repeatedly. - this.v = unwrappedDelegate.getValue(); - this.s = unwrappedDelegate.toString(); // names will do it too - this.delegateSupplier = () -> unwrappedDelegate; + unwrappedDelegate.toString(); // names will do it too + this.delegateSupplier = () -> unwrappedDelegate; // replace ourselves safely under lock } return unwrappedDelegate; }; } else { assert delegate instanceof UniversalAnnotationValue; // Symbol completion already happened; no lock needed - final UniversalAnnotationValue uav = (UniversalAnnotationValue)delegate; - this.v = uav.getValue(); // already cached/computed - this.s = uav.toString(); // already cached/computed this.delegateSupplier = () -> unwrappedDelegate; } } @@ -165,30 +160,25 @@ public final boolean equals(final Object other) { @Override // AnnotationValue @SuppressWarnings("unchecked") public final Object getValue() { - final PrimordialDomain domain = this.domain(); - final Object value = this.v; + final Object value = this.delegate().getValue(); return switch (value) { case null -> throw new AssertionError(); - case AnnotationMirror a -> UniversalAnnotation.of(a, domain); - case List l -> of((List)l, domain); - case TypeMirror t -> UniversalType.of(t, domain); - case VariableElement e -> UniversalElement.of(e, domain); + case AnnotationMirror a -> UniversalAnnotation.of(a, this.domain()); + case List l -> of((List)l, this.domain()); + case TypeMirror t -> UniversalType.of(t, this.domain()); + case VariableElement e -> UniversalElement.of(e, this.domain()); default -> value; }; } @Override // Object - @SuppressWarnings({ "try", "unchecked" }) public final int hashCode() { - // No lock needed; see - // https://github.com/openjdk/jdk/blob/jdk-26%2B25/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Attribute.java#L45 return this.delegate().hashCode(); } @Override // AnnotationValue - @SuppressWarnings("try") public final String toString() { - return this.s; + return this.delegate().toString(); } @@ -260,5 +250,4 @@ public static final AnnotationValue unwrap(AnnotationValue a) { return a; } - } diff --git a/src/main/java/org/microbean/construct/element/UniversalDirective.java b/src/main/java/org/microbean/construct/element/UniversalDirective.java index 94d4934..6d22803 100644 --- a/src/main/java/org/microbean/construct/element/UniversalDirective.java +++ b/src/main/java/org/microbean/construct/element/UniversalDirective.java @@ -1,6 +1,6 @@ /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- * - * Copyright © 2024–2025 microBean™. + * Copyright © 2024–2026 microBean™. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -15,9 +15,7 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.function.Supplier; @@ -32,6 +30,10 @@ import org.microbean.construct.PrimordialDomain; +import static java.util.Collections.unmodifiableList; + +import static java.util.Objects.requireNonNull; + /** * A {@link Directive} implementation. * @@ -60,12 +62,11 @@ public final class UniversalDirective @SuppressWarnings("try") public UniversalDirective(final Directive delegate, final PrimordialDomain domain) { super(); - this.domain = Objects.requireNonNull(domain, "domain"); - final Directive unwrappedDelegate = unwrap(Objects.requireNonNull(delegate, "delegate")); - final Runnable symbolCompleter = unwrappedDelegate::getKind; + this.domain = requireNonNull(domain, "domain"); + final Directive unwrappedDelegate = unwrap(requireNonNull(delegate, "delegate")); this.delegateSupplier = () -> { try (var lock = domain.lock()) { - symbolCompleter.run(); + unwrappedDelegate.toString(); // symbol completion this.delegateSupplier = () -> unwrappedDelegate; } return unwrappedDelegate; @@ -228,7 +229,7 @@ public static final List of(final Collection implements ExecutableElement, @@ -88,9 +87,9 @@ public UniversalElement(final Element delegate, final PrimordialDomain domain) { * * @param delegate an {@link Element} to which operations will be delegated; must not be {@code null} * - * @param annotations a {@link List} of {@link AnnotationMirror} instances representing annotations, often synthetic, - * that this {@link UniversalElement} should bear; may be {@code null} in which case only the annotations from the - * supplied {@code delegate} will be used + * @param annotations a {@link List} of {@link AnnotationMirror} instances representing annotations, often {@linkplain + * SyntheticAnnotationMirror synthetic}, that this {@link UniversalElement} should bear; may be {@code null} in which + * case only the annotations from the supplied {@code delegate} will be used * * @param domain a {@link PrimordialDomain} from which the supplied {@code delegate} is presumed to have originated; * must not be {@code null} @@ -435,7 +434,7 @@ public static final List of(final Collection R accept(final TypeVisitor v, final P p) { protected UniversalType annotate(final List replacementAnnotations) { return new UniversalType(this.delegate(), replacementAnnotations, this.domain()); } - + @Override // Various public final UniversalElement asElement() { return switch (this.getKind()) { @@ -225,7 +223,7 @@ public final UniversalType getEnclosingType() { public final UniversalType getExtendsBound() { return switch (this.getKind()) { case WILDCARD -> this.wrap(((WildcardType)this.delegate()).getExtendsBound()); - default -> null; + default -> null; // TODO: ...or simply this, strictly speaking }; } @@ -246,7 +244,7 @@ public final UniversalType getLowerBound() { public final UniversalType getUpperBound() { return switch (this.getKind()) { case TYPEVAR -> this.wrap(((TypeVariable)this.delegate()).getUpperBound()); - default -> this.wrap(this.domain().javaLangObjectType()); + default -> this.wrap(this.domain().noType(NONE)); // experimental }; } @@ -270,7 +268,7 @@ public final UniversalType getReceiverType() { public final UniversalType getReturnType() { return switch (this.getKind()) { case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate()).getReturnType()); - default -> this.wrap(this.domain().noType(VOID)); + default -> this.wrap(this.domain().noType(VOID)); // Of note: this is also appropriate when this is not a method }; } diff --git a/src/test/java/org/microbean/construct/element/TestTypes.java b/src/test/java/org/microbean/construct/element/TestTypes.java new file mode 100644 index 0000000..f7ecc83 --- /dev/null +++ b/src/test/java/org/microbean/construct/element/TestTypes.java @@ -0,0 +1,79 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2026 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.construct.element; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Test; + +import org.microbean.construct.DefaultDomain; + +import static javax.lang.model.type.TypeKind.ARRAY; +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.VOID; + +import static javax.lang.model.util.ElementFilter.methodsIn; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.construct.element.AnnotationMirrors.get; + +@TestTypes.Gorp +final class TestTypes { + + private static final DefaultDomain domain = new DefaultDomain(); + + private TestTypes() { + super(); + } + + @Test + final void testTypeOfIntArrayClass() { + final AnnotationMirror am = domain.typeElement(this.getClass().getCanonicalName()).getAnnotationMirrors().get(0); + final TypeMirror t = (TypeMirror)get(am, "iac"); + assertTrue(t instanceof ArrayType); + assertSame(ARRAY, t.getKind()); + } + + @Test + final void testTypeOfVoidClass() { + final AnnotationMirror am = domain.typeElement(this.getClass().getCanonicalName()).getAnnotationMirrors().get(0); + final TypeMirror t = (TypeMirror)get(am, "vc"); + assertTrue(t instanceof NoType); + assertSame(VOID, t.getKind()); + } + + @Test + final void testAnnotationMirrorRepresentationOfElementReturnTypes() { + final AnnotationMirror am = domain.typeElement(this.getClass().getCanonicalName()).getAnnotationMirrors().get(0); + for (final ExecutableElement ee : methodsIn(am.getAnnotationType().asElement().getEnclosedElements())) { + assertSame(DECLARED, ee.getReturnType().getKind()); + } + } + + static @interface Gorp { + + Class iac() default int[].class; + + Class vc() default void.class; + + } + +}