From 67cc557b6da037db09088858b33d58ec2b8db1f2 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Thu, 5 Feb 2026 18:02:15 -0800 Subject: [PATCH] Adds synthetic constructs. General cleanup. Signed-off-by: Laird Nelson --- README.md | 2 +- pom.xml | 4 +- .../element/SyntheticAnnotationMirror.java | 51 ++++- .../SyntheticAnnotationTypeElement.java | 61 +++++- .../SyntheticLocalVariableElement.java | 196 ++++++++++++++++++ .../construct/element/SyntheticName.java | 12 ++ .../TestSyntheticAnnotationMirror.java | 75 +++++++ 7 files changed, 389 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/microbean/construct/element/SyntheticLocalVariableElement.java create mode 100644 src/test/java/org/microbean/construct/element/TestSyntheticAnnotationMirror.java diff --git a/README.md b/README.md index bb52726..f751ace 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ dependency: Always check https://search.maven.org/artifact/org.microbean/microbean-construct for up-to-date available versions. --> - 0.0.21 + 0.0.22 ``` diff --git a/pom.xml b/pom.xml index a5e221b..9080762 100644 --- a/pom.xml +++ b/pom.xml @@ -274,7 +274,7 @@ maven-compiler-plugin - 3.14.1 + 3.15.0 -Xlint:all @@ -369,7 +369,7 @@ org.codehaus.mojo versions-maven-plugin - 2.20.1 + 2.21.0 io.smallrye diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java index e430e44..a9ae819 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java +++ b/src/main/java/org/microbean/construct/element/SyntheticAnnotationMirror.java @@ -18,9 +18,11 @@ import java.lang.constant.ConstantDesc; import java.lang.constant.DynamicConstantDesc; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import javax.lang.model.element.AnnotationMirror; @@ -69,7 +71,7 @@ public final class SyntheticAnnotationMirror implements AnnotationMirror, Consta private final TypeElement annotationTypeElement; - private final Map elementValues; + private final Map elementValues; /* @@ -142,6 +144,39 @@ public SyntheticAnnotationMirror(final TypeElement annotationTypeElement, this.elementValues = m.isEmpty() ? Map.of() : unmodifiableMap(m); } + /** + * Creates a new {@link SyntheticAnnotationMirror} that is an effective copy of the supplied {@link + * AnnotationMirror}. + * + * @param a a non-{@code null} {@link AnnotationMirror} to semantically copy + * + * @exception NullPointerException if {@code a} is {@code null} + */ + public SyntheticAnnotationMirror(final AnnotationMirror a) { + super(); + this.annotationTypeElement = new SyntheticAnnotationTypeElement((TypeElement)a.getAnnotationType().asElement()); + final Map originalElementValues = a.getElementValues(); + if (originalElementValues.isEmpty()) { + // If there are no explicit values, then...there are no explicit values whether the annotation interface type + // contains/encloses any elements at all. + this.elementValues = Map.of(); + } else { + // There are explicit values. That also means that the annotation interface type contains/encloses at least one + // element. + final List syntheticElements = methodsIn(this.annotationTypeElement.getEnclosedElements()); + assert !syntheticElements.isEmpty(); + final Map newElementValues = newLinkedHashMap(originalElementValues.size()); + for (final Entry originalEntry : originalElementValues.entrySet()) { + final ExecutableElement originalElement = originalEntry.getKey(); + final ExecutableElement syntheticElement = element(syntheticElements, originalElement.getSimpleName()); + if (syntheticElement != null) { + newElementValues.put(syntheticElement, originalEntry.getValue()); + } + } + this.elementValues = unmodifiableMap(newElementValues); + } + } + /* * Instance methods. @@ -172,6 +207,11 @@ public final DeclaredType getAnnotationType() { return this.elementValues; } + @Override + public final String toString() { + return "@" + this.annotationTypeElement.toString(); // TODO: not anywhere near good enough + } + /* * Static methods. @@ -197,4 +237,13 @@ private static final Optional describeExecutableElement( }; } + private static final E element(final Iterable elements, final CharSequence simpleName) { + for (final E e : elements) { + if (e.getSimpleName().contentEquals(simpleName)) { + return e; + } + } + return null; + } + } diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java index 067d367..f135ace 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java +++ b/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java @@ -58,6 +58,8 @@ import static javax.lang.model.type.TypeKind.ARRAY; import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.util.ElementFilter.methodsIn; + import static org.microbean.construct.element.AnnotationMirrors.validAnnotationInterfaceElementScalarType; /** @@ -285,6 +287,46 @@ public SyntheticAnnotationTypeElement(final List ann this(annotationMirrors, new SyntheticName(fullyQualifiedName), elements); } + /** + * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link + * SyntheticAnnotationMirror} instances. + * + * @param e a non-{@code null} {@link TypeElement} whose {@link TypeElement#getKind() getKind()} method returns {@link + * javax.lang.model.element.ElementKind#ANNOTATION_TYPE} + * + * @exception NullPointerException if {@code e} is {@code null} + * + * @exception IllegalArgumentException if the supplied {@link TypeElement}'s {@link TypeElement#getKind() getKind()} + * method does not return {@link javax.lang.model.element.ElementKind#ANNOTATION_TYPE} + */ + public SyntheticAnnotationTypeElement(final TypeElement e) { + super(); + if (e.getKind() != ANNOTATION_TYPE) { + throw new IllegalArgumentException("e: " + e); + } + this.annotationMirrors = new CopyOnWriteArrayList<>(e.getAnnotationMirrors()); + Name n = e.getSimpleName(); + this.sn = n instanceof SyntheticName sn ? sn : new SyntheticName(n.toString()); + n = e.getQualifiedName(); + this.fqn = n instanceof SyntheticName sn ? sn : new SyntheticName(n.toString()); + // Deliberate: Type is an inner class and hence cannot be shared + this.type = new Type(); + final List elements = methodsIn(e.getEnclosedElements()); + if (elements.isEmpty()) { + this.elements = List.of(); + } else { + final List elements0 = new ArrayList<>(elements.size()); + for (final ExecutableElement ee :elements) { + // Deliberate: no instanceof InternalAnnotationElement check, i.e. wrapping/copying is deliberate and necessary + elements0.add(new InternalAnnotationElement(ee.getAnnotationMirrors(), + ee.getReturnType(), + ee.getSimpleName(), + ee.getDefaultValue())); + } + this.elements = unmodifiableList(elements0); + } + } + /** * Creates a new {@link SyntheticAnnotationTypeElement}, mostly, if not exclusively, for use by {@link * SyntheticAnnotationMirror} instances. @@ -320,8 +362,8 @@ public SyntheticAnnotationTypeElement(final List ann 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())); + for (final SyntheticAnnotationElement sae : elements) { + elements0.add(new InternalAnnotationElement(sae.annotationMirrors(), sae.type(), sae.name(), sae.defaultValue())); } this.elements = unmodifiableList(elements0); } @@ -572,7 +614,7 @@ public SyntheticAnnotationElement(final List annotat case ArrayType t when t.getKind() == ARRAY -> validateScalarType(t.getComponentType()); default -> validateScalarType(type); }; - if (name.equals("getClass") || name.equals("hashCode") || name.equals("toString")) { + if (name.contentEquals("getClass") || name.contentEquals("hashCode") || name.contentEquals("toString")) { // java.lang.Object-declared methods that might otherwise meet annotation element requirements throw new IllegalArgumentException("name: " + name); } @@ -728,13 +770,15 @@ private final class InternalAnnotationElement implements ExecutableElement { private InternalAnnotationElement(final List annotationMirrors, final TypeMirror type, - final SyntheticName name, - final SyntheticAnnotationValue defaultValue) { + final Name name, + final AnnotationValue defaultValue) { super(); this.annotationMirrors = new CopyOnWriteArrayList<>(annotationMirrors); - this.t = new Type(type); - this.name = requireNonNull(name, "name"); - this.defaultValue = defaultValue; + // This Type is NOT an inner class and cannot receive annotations so this is OK. + this.t = type instanceof Type t ? t : new Type(type); + this.name = name instanceof SyntheticName sn ? sn : new SyntheticName(name); + this.defaultValue = + defaultValue instanceof SyntheticAnnotationValue sav ? sav : new SyntheticAnnotationValue(defaultValue.getValue()); } @@ -861,6 +905,7 @@ private Type(final TypeMirror type) { // the "return type" this.type = switch (type) { case null -> throw new NullPointerException("type"); case ArrayType t when t.getKind() == ARRAY -> validateScalarType(t.getComponentType()); + case Type t -> t.type; default -> validateScalarType(type); }; } diff --git a/src/main/java/org/microbean/construct/element/SyntheticLocalVariableElement.java b/src/main/java/org/microbean/construct/element/SyntheticLocalVariableElement.java new file mode 100644 index 0000000..786c210 --- /dev/null +++ b/src/main/java/org/microbean/construct/element/SyntheticLocalVariableElement.java @@ -0,0 +1,196 @@ +/* -*- 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.annotation.Annotation; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.TypeMirror; + +import static java.util.Objects.requireNonNull; + +import static javax.lang.model.element.ElementKind.LOCAL_VARIABLE; + +/** + * An experimental {@link VariableElement} implementation that is a synthetic representation of a local + * variable. + * + *

{@link SyntheticLocalVariableElement} instances may be useful for capturing declaration annotations that really + * pertain to type usage. Such scenarios are often found in dependency injection systems.

+ * + * @author Laird Nelson + */ +public final class SyntheticLocalVariableElement implements VariableElement { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private final List annotationMirrors; + + private final Name name; + + private final TypeMirror type; + + /** + * Creates a new {@link SyntheticLocalVariableElement}. + * + * @param type a non-{@code null} {@link TypeMirror} that a hypothetical local variable may bear; as of this writing + * no validation is performed on any argument supplied for this parameter + * + * @exception NullPointerException if {@code type} is {@code null} + * + * @see #SyntheticLocalVariableElement(Collection, TypeMirror, String) + */ + public SyntheticLocalVariableElement(final TypeMirror type) { + this(List.of(), type, null); + } + + /** + * Creates a new {@link SyntheticLocalVariableElement}. + * + * @param type a non-{@code null} {@link TypeMirror} that a hypothetical local variable may bear; as of this writing + * no validation is performed on any argument supplied for this parameter + * + * @param name the name of this {@link SyntheticLocalVariableElement}; may be {@code null} + * + * @exception NullPointerException if {@code type} is {@code null} + * + * @see #SyntheticLocalVariableElement(Collection, TypeMirror, String) + */ + public SyntheticLocalVariableElement(final TypeMirror type, final String name) { + this(List.of(), type, name); + } + + /** + * Creates a new {@link SyntheticLocalVariableElement}. + * + * @param as a non-{@code null} {@link Collection} of {@link AnnotationMirror}s + * + * @param type a non-{@code null} {@link TypeMirror} that a hypothetical local variable may bear; as of this writing + * no validation is performed on any argument supplied for this parameter + * + * @exception NullPointerException if {@code as} or {@code type} is {@code null} + * + * @see #SyntheticLocalVariableElement(Collection, TypeMirror, String) + */ + public SyntheticLocalVariableElement(final Collection as, final TypeMirror type) { + this(as, type, null); + } + + /** + * Creates a new {@link SyntheticLocalVariableElement}. + * + * @param as a non-{@code null} {@link Collection} of {@link AnnotationMirror}s + * + * @param type a non-{@code null} {@link TypeMirror} that a hypothetical local variable may bear; as of this writing + * no validation is performed on any argument supplied for this parameter + * + * @param name the name of this {@link SyntheticLocalVariableElement}; may be {@code null} + * + * @exception NullPointerException if {@code as} or {@code type} is {@code null} + */ + public SyntheticLocalVariableElement(final Collection as, + final TypeMirror type, + final String name) { + super(); + this.annotationMirrors = new CopyOnWriteArrayList<>(as); + this.name = new SyntheticName(name == null ? "" : name); + this.type = requireNonNull(type, "type"); + } + + @Override // VariableElement (Element) + public final R accept(final ElementVisitor v, final P p) { + return v.visitVariable(this, p); + } + + @Override // VariableElement (Element) + public final TypeMirror asType() { + return this.type; + } + + @Override // VariableElement (Object) + public final boolean equals(final Object other) { + return this == other || switch (other) { + case null -> false; + case SyntheticLocalVariableElement her when this.getClass() == her.getClass() -> this.type.equals(her.type) && this.name.contentEquals(her.name); + default -> false; + }; + } + + @Override // VariableElement (AnnotatedConstruct) + public final List getAnnotationMirrors() { + return this.annotationMirrors; + } + + @Deprecated + @Override // VariableElement (AnnotatedConstruct) + public final A getAnnotation(final Class annotationType) { + return null; // deliberate + } + + @Deprecated + @Override // VariableElement (AnnotatedConstruct) + @SuppressWarnings("unchecked") + public final A[] getAnnotationsByType(final Class annotationType) { + return (A[])EMPTY_ANNOTATION_ARRAY; // deliberate + } + + @Override // VariableElement + public final Object getConstantValue() { + return null; + } + + @Override // VariableElement (Element) + public final List getEnclosedElements() { + return List.of(); + } + + @Override // VariableElement (Element) + public final Element getEnclosingElement() { + return null; // deliberate + } + + @Override // VariableElement (Element) + public final ElementKind getKind() { + return LOCAL_VARIABLE; + } + + @Override // VariableElement (Element) + public final Set getModifiers() { + return Set.of(); + } + + @Override // VariableElement (Element) + public final Name getSimpleName() { + return this.name; + } + + @Override // Object + public final int hashCode() { + return this.name.hashCode() ^ this.type.hashCode(); + } + +} diff --git a/src/main/java/org/microbean/construct/element/SyntheticName.java b/src/main/java/org/microbean/construct/element/SyntheticName.java index 98dde45..beefc96 100644 --- a/src/main/java/org/microbean/construct/element/SyntheticName.java +++ b/src/main/java/org/microbean/construct/element/SyntheticName.java @@ -61,6 +61,18 @@ public final class SyntheticName implements Constable, Name { */ + /** + * Creates a new {@link SyntheticName}. + * + * @param value the actual name; must not be {@code null} + * + * @exception NullPointerException if {@code value} is {@code null} + */ + public SyntheticName(final Name value) { + super(); + this.value = value instanceof SyntheticName sn ? sn.value : value.toString(); + } + /** * Creates a new {@link SyntheticName}. * diff --git a/src/test/java/org/microbean/construct/element/TestSyntheticAnnotationMirror.java b/src/test/java/org/microbean/construct/element/TestSyntheticAnnotationMirror.java new file mode 100644 index 0000000..8a0b104 --- /dev/null +++ b/src/test/java/org/microbean/construct/element/TestSyntheticAnnotationMirror.java @@ -0,0 +1,75 @@ +/* -*- 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.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +import org.junit.jupiter.api.Test; + +import org.microbean.construct.DefaultDomain; +import org.microbean.construct.Domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Deprecated(since = "never") // used by a test in this suite; this class is not actually deprecated +final class TestSyntheticAnnotationMirror { + + private TestSyntheticAnnotationMirror() { + super(); + } + + @Test + final void test() { + final AnnotationMirror deprecated = + new DefaultDomain().typeElement(this.getClass().getCanonicalName()).getAnnotationMirrors().get(0); + final TypeElement deprecatedTE = (TypeElement)deprecated.getAnnotationType().asElement(); + + final List elements = deprecatedTE.getEnclosedElements(); + assertEquals(2, elements.size()); // since, forRemoval + + ExecutableElement ee = (ExecutableElement)elements.get(0); + assertTrue(ee.getSimpleName().contentEquals("since")); // declaration order is retained per contract + ee = (ExecutableElement)elements.get(1); + assertTrue(ee.getSimpleName().contentEquals("forRemoval")); + + final AnnotationMirror syntheticDeprecated = new SyntheticAnnotationMirror(deprecated); + + final Map originalValues = deprecated.getElementValues(); + final Map syntheticValues = syntheticDeprecated.getElementValues(); + assertEquals(originalValues.size(), syntheticValues.size()); + assertNotEquals(originalValues, syntheticValues); + + final SyntheticAnnotationTypeElement syntheticDeprecatedTE = + (SyntheticAnnotationTypeElement)syntheticDeprecated.getAnnotationType().asElement(); + assertTrue(deprecatedTE.getQualifiedName().contentEquals(syntheticDeprecatedTE.getQualifiedName())); + + final List metaAnnotations = syntheticDeprecatedTE.getAnnotationMirrors(); + assertEquals(3, metaAnnotations.size()); // @Documented, @Retention, @Target + assertEquals(deprecatedTE.getAnnotationMirrors(), metaAnnotations); // note that synthetic copies weren't made + + syntheticDeprecatedTE.getAnnotationMirrors().clear(); // annotations are mutable on synthetics + assertEquals(0, metaAnnotations.size()); // annotation collections that are returned are thread-safe and mutable + assertEquals(3, deprecatedTE.getAnnotationMirrors().size()); // this was unaffected + } + +}