From a3d1dca19d0e52d05c3f1b9df4837dd895cddc13 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Thu, 22 Jan 2026 20:44:07 -0800 Subject: [PATCH] Road grading; adds annotation-related features Signed-off-by: Laird Nelson --- README.md | 2 +- pom.xml | 108 ++++++ .../microbean/construct/DefaultDomain.java | 8 + .../java/org/microbean/construct/Domain.java | 132 +++++-- .../construct/element/AnnotationMirrors.java | 362 +++++++++++++++--- .../SyntheticAnnotationTypeElement.java | 22 +- 6 files changed, 527 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 499c8b9..bb52726 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.20 + 0.0.21 ``` diff --git a/pom.xml b/pom.xml index 86d6f13..37bffde 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,114 @@ maven-checkstyle-plugin 3.6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project.basedir=${project.basedir} + project.build.sourceEncoding=${project.build.sourceEncoding} + + + + + com.puppycrawl.tools + checkstyle + 13.0.0 + + maven-clean-plugin diff --git a/src/main/java/org/microbean/construct/DefaultDomain.java b/src/main/java/org/microbean/construct/DefaultDomain.java index c8fdb84..5fdb52e 100644 --- a/src/main/java/org/microbean/construct/DefaultDomain.java +++ b/src/main/java/org/microbean/construct/DefaultDomain.java @@ -152,6 +152,14 @@ public DefaultDomain(final ProcessingEnvironment pe, final Lock lock) { */ + @Override // Domain + public List allMembers(TypeElement e) { + e = unwrap(e); + try (var lock = lock()) { + return UniversalElement.of(this.elements().getAllMembers(e), this); + } + } + /** * Returns a {@link UniversalType} representing an {@link javax.lang.model.type.ArrayType} whose {@linkplain * javax.lang.model.type.ArrayType#getComponentType() component type} {@linkplain #sameType(TypeMirror, TypeMirror) is diff --git a/src/main/java/org/microbean/construct/Domain.java b/src/main/java/org/microbean/construct/Domain.java index d9c033b..b13968f 100644 --- a/src/main/java/org/microbean/construct/Domain.java +++ b/src/main/java/org/microbean/construct/Domain.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 @@ -23,7 +23,8 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; + +import javax.lang.model.AnnotatedConstruct; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -53,12 +54,15 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Elements.Origin; +import org.microbean.construct.element.AnnotationMirrors; import org.microbean.construct.element.UniversalElement; import org.microbean.construct.type.UniversalType; import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; + import static javax.lang.model.type.TypeKind.DECLARED; /** @@ -96,6 +100,10 @@ public interface Domain extends PrimordialDomain { * Returns an immutable, determinate, non-{@code null} {@link List} of {@link AnnotationMirror} instances representing * all annotations present on an element, whether directly present or present via inheritance. * + *

The default implementation of this method calls {@link UniversalElement#of(Element, Domain)} with the supplied + * {@code element} and {@code this} as arguments, calls the {@link + * AnnotationMirrors#allAnnotationMirrors(Element)} with the result, and returns the result.

+ * * @param element the {@link Element} whose present annotations should be returned; must not be {@code null} * * @return an immutable, determinate, non-{@code null} {@link List} of {@link AnnotationMirror} instances representing @@ -103,37 +111,91 @@ public interface Domain extends PrimordialDomain { * * @exception NullPointerException if {@code element} is {@code null} * + * @see AnnotationMirrors#allAnnotationMirrors(Element) + * * @see javax.lang.model.util.Elements#getAllAnnotationMirrors(Element) */ - public default List allAnnotationMirrors(Element element) { - element = UniversalElement.of(element, this); // handles locking, symbol completion, etc. - final List annotations = new ArrayList<>(8); - annotations.addAll(element.getAnnotationMirrors().reversed()); - TypeMirror sc = ((TypeElement)element).getSuperclass(); - if (sc.getKind() == DECLARED) { - element = ((DeclaredType)sc).asElement(); - WHILE_LOOP: - while (element != null && element.getKind().isDeclaredType()) { - for (final AnnotationMirror a : element.getAnnotationMirrors().reversed()) { - // See if it's inherited - final TypeElement annotationTypeElement = (TypeElement)a.getAnnotationType().asElement(); - for (final AnnotationMirror metaAnnotation : annotationTypeElement.getAnnotationMirrors()) { - if (((TypeElement)metaAnnotation.getAnnotationType().asElement()).getQualifiedName().contentEquals("java.lang.annotation.Inherited")) { - for (final AnnotationMirror existingAnnotation : annotations) { - if (existingAnnotation.getAnnotationType().asElement().equals(annotationTypeElement)) { - continue WHILE_LOOP; - } - } - annotations.add(a); - break; - } - } - } - sc = ((TypeElement)element).getSuperclass(); - element = sc.getKind() == DECLARED ? ((DeclaredType)sc).asElement() : null; - } - } - return annotations.isEmpty() ? List.of() : unmodifiableList(annotations.reversed()); + public default List allAnnotationMirrors(final Element element) { + return AnnotationMirrors.allAnnotationMirrors(UniversalElement.of(element, this)); // handles locking, symbol completion + } + + /** + * Returns a non-{@code null}, determinate, immutable {@link List} whose elements comprise the members of + * the supplied {@link TypeElement}, whether they are inherited or declared directly. + * + *

If the supplied {@link TypeElement} is a {@linkplain ElementKind#CLASS class}, the returned {@link List} will + * also include the class' constructors, but not any local or anonymous classes.

+ * + * @param e a {@link TypeElement}; must not be {@code null} + * + * @return a non-{@code null}, determinate, immutable {@link List} whose elements comprise the members of + * the supplied {@link TypeElement}, whether they are inherited or declared directly + * + * @exception NullPointerException if {@code e} is {@code null} + * + * @see Elements#getAllMembers(TypeElement) + */ + public abstract List allMembers(TypeElement e); + + /** + * An experimental method that returns an {@link Element} that is {@linkplain Element#equals(Object) + * equal to} the supplied {@link Element} but {@linkplain Element#getAnnotationMirrors() with} a {@link List} of + * annotations {@linkplain List#equals(Object) equal to} the supplied {@link List} of {@link AnnotationMirror}s. + * + *

The supplied {@link Element} may be modified in place if the return value of its {@link + * Element#getAnnotationMirrors() getAnnotationMirrors()} method is mutable.

+ * + *

No validation of any kind is performed on either argument.

+ * + * @param annotations a {@link List} of {@link AnnotationMirror}s; must not be {@code null} + * + * @param e an {@link Element}; must not be {@code null} + * + * @return an {@link Element} that is {@linkplain Element#equals(Object) equal to} the supplied {@link Element} but + * {@linkplain Element#getAnnotationMirrors() with} a {@link List} of annotations {@linkplain List#equals(Object) + * equal to} the supplied {@link List} of {@link AnnotationMirror}s + * + * @exception NullPointerException if any argument is {@code null} + * + * @see UniversalElement#getAnnotationMirrors() + */ + public default Element annotate(final List annotations, final Element e) { + final UniversalElement ue = UniversalElement.of(e, this); + final List l = ue.getAnnotationMirrors(); + l.clear(); + l.addAll(annotations); + return ue; + } + + /** + * An experimental method that returns a {@link TypeMirror} that is {@linkplain + * TypeMirror#equals(Object) equal to} the supplied {@link TypeMirror} but {@linkplain + * TypeMirror#getAnnotationMirrors() with} a {@link List} of annotations {@linkplain List#equals(Object) equal to} the + * supplied {@link List} of {@link AnnotationMirror}s. + * + *

The supplied {@link TypeMirror} may be modified in place if the return value of its {@link + * TypeMirror#getAnnotationMirrors() getAnnotationMirrors()} method is mutable.

+ * + *

No validation of any kind is performed on either argument.

+ * + * @param annotations a {@link List} of {@link AnnotationMirror}s; must not be {@code null} + * + * @param t a {@link TypeMirror}; must not be {@code null} + * + * @return a {@link TypeMirror} that is {@linkplain TypeMirror#equals(Object) equal to} the supplied {@link + * TypeMirror} but {@linkplain TypeMirror#getAnnotationMirrors() with} a {@link List} of annotations {@linkplain + * List#equals(Object) equal to} the supplied {@link List} of {@link AnnotationMirror}s + * + * @exception NullPointerException if any argument is {@code null} + * + * @see UniversalType#getAnnotationMirrors() + */ + public default TypeMirror annotate(final List annotations, final TypeMirror t) { + final UniversalType ut = UniversalType.of(t, this); + final List l = ut.getAnnotationMirrors(); + l.clear(); + l.addAll(annotations); + return ut; } /** @@ -970,7 +1032,7 @@ public default boolean parameterized(final TypeMirror t) { // (Convenience.) // (Unboxing.) public default PrimitiveType primitiveType(final CharSequence canonicalName) { - final String s = this.toString(Objects.requireNonNull(canonicalName, "canonicalName")); + final String s = this.toString(requireNonNull(canonicalName, "canonicalName")); return switch (s) { case "boolean", "java.lang.Boolean" -> this.primitiveType(TypeKind.BOOLEAN); case "byte", "java.lang.Byte" -> this.primitiveType(TypeKind.BYTE); @@ -1462,8 +1524,8 @@ public default TypeElement typeElement(final TypeKind primitiveTypeKind) { */ // (Convenience.) public default TypeParameterElement typeParameterElement(Parameterizable p, final CharSequence name) { - Objects.requireNonNull(p, "p"); - Objects.requireNonNull(name, "name"); + requireNonNull(p, "p"); + requireNonNull(name, "name"); while (p != null) { switch (p) { case UniversalElement ue: @@ -1532,7 +1594,7 @@ public default TypeVariable typeVariable(Parameterizable p, final CharSequence n */ // (Convenience.) public default VariableElement variableElement(final Element enclosingElement, final CharSequence simpleName) { - Objects.requireNonNull(simpleName, "simpleName"); + requireNonNull(simpleName, "simpleName"); return switch (enclosingElement) { case null -> throw new NullPointerException("enclosingElement"); case UniversalElement ue -> { diff --git a/src/main/java/org/microbean/construct/element/AnnotationMirrors.java b/src/main/java/org/microbean/construct/element/AnnotationMirrors.java index 6f8df60..9c79830 100644 --- a/src/main/java/org/microbean/construct/element/AnnotationMirrors.java +++ b/src/main/java/org/microbean/construct/element/AnnotationMirrors.java @@ -40,11 +40,14 @@ import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; 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.PrimitiveType; import javax.lang.model.type.TypeMirror; import static java.util.Collections.unmodifiableList; @@ -63,7 +66,12 @@ import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; import static javax.lang.model.element.ElementKind.CLASS; +import static javax.lang.model.element.ElementKind.METHOD; +import static javax.lang.model.element.Modifier.ABSTRACT; +import static javax.lang.model.element.Modifier.PUBLIC; + +import static javax.lang.model.type.TypeKind.ARRAY; import static javax.lang.model.type.TypeKind.DECLARED; import static javax.lang.model.util.ElementFilter.methodsIn; @@ -107,7 +115,7 @@ public static final List allAnnotationMirrors(Elemen WHILE_LOOP: while (e.getKind() == CLASS && e instanceof TypeElement te) { final TypeMirror sct = te.getSuperclass(); - if (sct.getKind() != DECLARED) { + if (sct.getKind() != DECLARED || !(sct instanceof DeclaredType)) { break; } e = ((DeclaredType)sct).asElement(); @@ -136,11 +144,11 @@ public static final List allAnnotationMirrors(Elemen * 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.

+ * href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.1">annotation interface 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.

+ *

Each {@link AnnotationValue} represents the value of an annotation interface 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.

@@ -213,8 +221,9 @@ public static final AnnotationMirror get(final AnnotatedConstruct ac, final Char } /** - * 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. + * A convenience method that returns a value for an annotation interface element {@linkplain + * ExecutableElement#getSimpleName() 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} * @@ -228,12 +237,38 @@ public static final AnnotationMirror get(final AnnotatedConstruct ac, final Char * @see AnnotationValue * * @see #allAnnotationValues(AnnotationMirror) + * + * @see #get(Map, CharSequence) */ public static final Object get(final AnnotationMirror am, final CharSequence name) { + return name == null ? null : get(allAnnotationValues(am), name); + } + + /** + * A convenience method that returns a value for an annotation interface element {@linkplain + * ExecutableElement#getSimpleName() named} by the supplied {@link CharSequence} and found in the supplied {@link + * Map}, or {@code null} if no such value exists. + * + * @param values a {@link Map} as returned by the {@link #allAnnotationValues(AnnotationMirror)} method; 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 a suitable {@link AnnotationValue} + * found in the supplied {@link Map}, or {@code null} + * + * @exception NullPointerException if {@code values} is {@code null} + * + * @see AnnotationValue + * + * @see #allAnnotationValues(AnnotationMirror) + */ + public static final Object get(final Map values, + final CharSequence name) { if (name == null) { return null; } - for (final Entry e : allAnnotationValues(am).entrySet()) { + for (final Entry e : values.entrySet()) { if (e.getKey().getSimpleName().contentEquals(name)) { final AnnotationValue av = e.getValue(); return av == null ? null : av.getValue(); @@ -359,7 +394,7 @@ public static final boolean sameAnnotation(final AnnotationMirror am0, final Ann * @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 + * annotation interface 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) @@ -377,9 +412,6 @@ public static final boolean sameAnnotation(final AnnotationMirror am0, } 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())) { @@ -387,9 +419,15 @@ public static final boolean sameAnnotation(final AnnotationMirror am0, } final SequencedMap m0 = allAnnotationValues(am0); final SequencedMap m1 = allAnnotationValues(am1); - if (m0.size() != m1.size()) { + if (m0 == m1) { + // Unlikely, but hey + return true; + } else if (m0.size() != m1.size()) { return false; } + if (p == null) { + p = AnnotationMirrors::returnTrue; + } final Iterator> i0 = m0.entrySet().iterator(); final Iterator> i1 = m1.entrySet().iterator(); while (i0.hasNext()) { @@ -406,24 +444,22 @@ public static final boolean sameAnnotation(final AnnotationMirror am0, } /** - * Returns a non-{@code null}, sequential, non-empty {@link Stream} that traverses the supplied {@link - * AnnotationMirror}'s {@linkplain AnnotationMirror#getAnnotationType() annotation interface}'s {@link TypeElement}'s + * Returns a non-{@code null}, sequential, {@link Stream} that traverses the supplied {@link AnnotatedConstruct}'s * {@linkplain AnnotatedConstruct#getAnnotationMirrors() annotations}, and their (meta-) annotations, in breadth-first * order. * - *

The first and possibly only element in the {@link Stream} that is returned is guaranteed to be the supplied - * {@link AnnotationMirror}.

+ *

Cycles are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror)} method.

* - * @param am an {@link AnnotationMirror}; must not be {@code null} + * @param ac an {@link AnnotatedConstruct}; must not be {@code null} * * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s * - * @exception NullPointerException if {@code am} is {@code null} + * @exception NullPointerException if {@code ac} is {@code null} * - * @see #streamBreadthFirst(AnnotatedConstruct) + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) */ - public static final Stream streamBreadthFirst(final AnnotationMirror am) { - return concat(Stream.of(am), streamBreadthFirst(am.getAnnotationType().asElement())); + public static final Stream streamBreadthFirst(final AnnotatedConstruct ac) { + return streamBreadthFirst(ac, AnnotationMirrors::returnTrue); } /** @@ -431,54 +467,120 @@ public static final Stream streamBreadthFirst(final Annotation * {@linkplain AnnotatedConstruct#getAnnotationMirrors() annotations}, and their (meta-) annotations, in breadth-first * order. * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror, + * Predicate)} method.

+ * * @param ac an {@link AnnotatedConstruct}; must not be {@code null} * + * @param p a {@link Predicate} that returns {@code true} if a given {@link ExecutableElement}, representing an + * annotation interface element, is to be included in comparison operations; may be {@code null} in which case it is + * as if {@code ()-> true} were supplied instead + * * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s * * @exception NullPointerException if {@code ac} is {@code null} + * + * @see #streamBreadthFirst(Collection, Predicate) + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) */ - public static final Stream streamBreadthFirst(final AnnotatedConstruct ac) { - final List ams = ac.getAnnotationMirrors(); - final Set names = newHashSet(ams.size()); + public static final Stream streamBreadthFirst(final AnnotatedConstruct ac, + final Predicate p) { + return streamBreadthFirst(ac.getAnnotationMirrors(), p); + } + + /** + * Returns a non-{@code null}, sequential, {@link Stream} that traverses the supplied {@link AnnotationMirror}s, and + * their (meta-) annotations, in breadth-first order. + * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror)} + * method. Consequently the returned {@link Stream} yields only semantically distinct elements.

+ * + * @param ams a {@link Collection} of {@link AnnotationMirror}s; must not be {@code null} + * + * @return a non-{@code null} {@link Stream} of (distinct) {@link AnnotationMirror}s + * + * @exception NullPointerException if {@code ams} is {@code null} + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) + */ + public static final Stream streamBreadthFirst(final Collection ams) { + return streamBreadthFirst(ams, AnnotationMirrors::returnTrue); + } + + /** + * Returns a non-{@code null}, sequential, {@link Stream} that traverses the supplied {@link AnnotationMirror}s, and + * their (meta-) annotations, in breadth-first order. + * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror, + * Predicate)} method. Consequently the returned {@link Stream} yields only semantically distinct elements.

+ * + * @param ams a {@link Collection} of {@link AnnotationMirror}s; must not 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 comparison operations; may be {@code null} in which case it is as if + * {@code ()-> true} were supplied instead + * + * @return a non-{@code null} {@link Stream} of (distinct) {@link AnnotationMirror}s + * + * @exception NullPointerException if {@code ams} is {@code null} + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror, Predicate) + */ + public static final Stream streamBreadthFirst(final Collection ams, + final Predicate p) { + if (ams.isEmpty()) { + return empty(); + } + final Collection seen = new ArrayList<>(); final Queue q = new ArrayDeque<>(); - ams.forEach(a -> { - names.add(((QualifiedNameable)a.getAnnotationType().asElement()).getQualifiedName().toString()); - q.add(a); - }); + DEDUP_LOOP_0: + for (final AnnotationMirror a0 : ams) { + for (final AnnotationMirror a1 : seen) { + if (sameAnnotation(a0, a1, p)) { + continue DEDUP_LOOP_0; + } + } + q.add(a0); + seen.add(a0); + } return iterate(q.poll(), Objects::nonNull, a0 -> { - a0.getAnnotationType() - .asElement() - .getAnnotationMirrors() - .stream() - .filter(a1 -> names.add(((QualifiedNameable)a1.getAnnotationType().asElement()).getQualifiedName().toString())) - .forEach(q::add); + DEDUP_LOOP_1: + for (final AnnotationMirror a1 : a0.getAnnotationType().asElement().getAnnotationMirrors()) { + for (final AnnotationMirror a2 : seen) { + if (sameAnnotation(a1, a2, p)) { + continue DEDUP_LOOP_1; + } + } + q.add(a1); + seen.add(a1); + } return q.poll(); }); } /** - * A convenience method that returns a non-{@code null}, sequential, non-empty {@link Stream} that traverses the - * supplied {@link AnnotationMirror}'s {@linkplain AnnotationMirror#getAnnotationType() annotation interface}'s {@link - * TypeElement}'s {@linkplain AnnotatedConstruct#getAnnotationMirrors() annotations}, and their (meta-) annotations, - * in depth-first order. + * Returns a non-{@code null}, sequential {@link Stream} that traverses the supplied {@link AnnotatedConstruct}'s + * {@linkplain AnnotatedConstruct#getAnnotationMirrors() annotations}, and their (meta-) annotations, in depth-first + * order. * - *

The first and possibly only element in the {@link Stream} that is returned is guaranteed to be the supplied - * {@link AnnotationMirror}.

+ *

Cycles are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror)} method.

* - * @param am an {@link AnnotationMirror}; must not be {@code null} + * @param ac an {@link AnnotatedConstruct}; must not be {@code null} * * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s * - * @exception NullPointerException if {@code am} is {@code null} + * @exception NullPointerException if {@code ac} is {@code null} * - * @see #streamDepthFirst(AnnotatedConstruct) + * @see #streamDepthFirst(AnnotatedConstruct, Predicate) + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) */ - // (Convenience.) - public static final Stream streamDepthFirst(final AnnotationMirror am) { - return concat(Stream.of(am), streamDepthFirst(am.getAnnotationType().asElement())); + public static final Stream streamDepthFirst(final AnnotatedConstruct ac) { + return streamDepthFirst(ac, AnnotationMirrors::returnTrue); // 17 == arbitrary } /** @@ -486,16 +588,72 @@ public static final Stream streamDepthFirst(final AnnotationMi * {@linkplain AnnotatedConstruct#getAnnotationMirrors() annotations}, and their (meta-) annotations, in depth-first * order. * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror, Predicate)} + * method.

+ * * @param ac an {@link AnnotatedConstruct}; must not 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 comparison operations; may be {@code null} in which case it is as if + * {@code ()-> true} were supplied instead + * * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s * * @exception NullPointerException if {@code ac} is {@code null} + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) */ - public static final Stream streamDepthFirst(final AnnotatedConstruct ac) { - return streamDepthFirst(ac, newHashSet(17)); // 17 == arbitrary + public static final Stream streamDepthFirst(final AnnotatedConstruct ac, + final Predicate p) { + return streamDepthFirst(ac, new ArrayList<>(17), p); // 17 == arbitrary + } + + /** + * Returns a non-{@code null}, sequential {@link Stream} that traverses the supplied {@link AnnotationMirror}s, and + * their (meta-) annotations, in depth-first order. + * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror)} + * method.

+ * + * @param ams a non-{@code null} {@link Collection} of {@link AnnotationMirror}s + * + * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s + * + * @exception NullPointerException if {@code ams} is {@code null} + * + * @see #streamDepthFirst(Collection, Predicate) + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) + */ + public static final Stream streamDepthFirst(final Collection ams) { + return streamDepthFirst(ams, AnnotationMirrors::returnTrue); // 17 == arbitrary + } + + /** + * Returns a non-{@code null}, sequential {@link Stream} that traverses the supplied {@link AnnotationMirror}s, and + * their (meta-) annotations, in depth-first order. + * + *

Cycles and duplicates are avoided via usage of the {@link #sameAnnotation(AnnotationMirror, AnnotationMirror, + * Predicate)} method.

+ * + * @param ams a non-{@code null} {@link Collection} of {@link AnnotationMirror}s + * + * @param p a {@link Predicate} that returns {@code true} if a given {@link ExecutableElement}, representing an + * annotation element, is to be included in comparison operations; may be {@code null} in which case it is as if + * {@code ()-> true} were supplied instead + * + * @return a non-{@code null} {@link Stream} of {@link AnnotationMirror}s + * + * @exception NullPointerException if {@code ams} is {@code null} + * + * @see #sameAnnotation(AnnotationMirror, AnnotationMirror) + */ + public static final Stream streamDepthFirst(final Collection ams, + final Predicate p) { + return ams.isEmpty() ? empty() : streamDepthFirst(ams, new ArrayList<>(17), p); // 17 == arbitrary } + /** * Returns a non-{@code null}, determinate, immutable {@link Set} of {@link ElementType}s describing restrictions * concerning where the annotation interface {@linkplain AnnotationMirror#getAnnotationType() represented} by the @@ -506,6 +664,8 @@ public static final Stream streamDepthFirst(final AnnotatedCon * @return a non-{@code null}, determinate, immutable {@link Set} of {@link ElementType}s describing restrictions * concerning where the annotation interface {@linkplain AnnotationMirror#getAnnotationType() represented} by the * supplied {@link AnnotationMirror} may be applied + * + * @exception NullPointerException if {@code a} is {@code null} */ public static final Set targetElementTypes(final AnnotationMirror a) { return targetElementTypes((TypeElement)a.getAnnotationType().asElement()); @@ -521,6 +681,8 @@ public static final Set targetElementTypes(final AnnotationMirror a * @return a non-{@code null}, determinate, immutable {@link Set} of {@link ElementType}s describing restrictions * concerning where the supplied annotation interface may be applied * + * @exception NullPointerException if {@code annotationInterface} is {@code null} + * * @see java.lang.annotation.ElementType * * @see java.lang.annotation.Target @@ -548,20 +710,112 @@ public static final Set targetElementTypes(final TypeElement annota return EMPTY_ELEMENT_TYPES; } + /** + * Returns {@code true} if and only if the supplied {@link ExecutableElement} represents a valid annotation + * interface element as defined by the Java Language Specification. + * + * @param e an {@link ExecutableElement}; must not be {@code null} + * + * @return {@code true} if and only if the supplied {@link ExecutableElement} represents a valid annotation + * interface element as defined by the Java Language Specification + * + * @exception NullPointerException if {@code e} is {@code null} + * + * @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 static final boolean validAnnotationInterfaceElement(final ExecutableElement e) { + return + e.getKind() == METHOD && + e.getEnclosingElement().getKind() == ANNOTATION_TYPE && + e.getTypeParameters().isEmpty() && + e.getParameters().isEmpty() && + e.getThrownTypes().isEmpty() && + validAnnotationInterfaceElementType(e.getReturnType()) && + (e.getModifiers().isEmpty() || + e.getModifiers().equals(EnumSet.of(ABSTRACT)) || + e.getModifiers().equals(EnumSet.of(PUBLIC))); + } + + /** + * Returns {@code true} if and only if the supplied {@link TypeMirror} is a valid type for an annotation + * interface element as defined by the Java Language Specification. + * + * @param t a {@link TypeMirror}; must not be {@code null} + * + * @return {@code true} if and only if the supplied {@link TypeMirror} is a valid type for an annotation + * interface element as defined by the Java Language Specification + * + * @exception NullPointerException if {@code t} is {@code null} + * + * @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 static final boolean validAnnotationInterfaceElementType(final TypeMirror t) { + return switch (t) { + case null -> throw new NullPointerException("t"); + case ArrayType at when at.getKind() == ARRAY -> validAnnotationInterfaceElementScalarType(at.getComponentType()); + default -> validAnnotationInterfaceElementScalarType(t); + }; + } + + static final boolean validAnnotationInterfaceElementScalarType(final TypeMirror t) { + return switch (t) { + case null -> throw new NullPointerException("t"); + case PrimitiveType pt when pt.getKind().isPrimitive() -> true; + case DeclaredType dt when dt.getKind() == DECLARED -> { + final TypeElement te = (TypeElement)dt.asElement(); + yield switch (te.getKind()) { + case ANNOTATION_TYPE, ENUM -> true; + case CLASS -> { + final Name fqn = te.getQualifiedName(); + yield fqn.contentEquals("java.lang.Class") || fqn.contentEquals("java.lang.String"); + } + default -> false; + }; + } + default -> false; + }; + } + @SuppressWarnings("unchecked") private static final SequencedMap emptySequencedMap() { return (SequencedMap)EMPTY_MAP; } + private static final boolean returnTrue(final X ignored) { + return true; + } + private static final Stream streamDepthFirst(final AnnotatedConstruct ac, - final Set names) { + final Collection seen, + final Predicate p) { + return streamDepthFirst(ac.getAnnotationMirrors(), seen, p); + } + + private static final Stream streamDepthFirst(final Collection ams, + final Collection seen, + final Predicate p) { + if (ams.isEmpty()) { + return empty(); + } // See https://www.techempower.com/blog/2016/10/19/efficient-multiple-stream-concatenation-in-java/ return - Stream.of(ac.getAnnotationMirrors() + Stream.of(ams .stream() - .flatMap(a -> { - final TypeElement e = (TypeElement)a.getAnnotationType().asElement(); - return names.add(e.getQualifiedName().toString()) ? streamDepthFirst(e, names) : empty(); + .sequential() + .flatMap(a0 -> { + for (final AnnotationMirror a1 : seen) { + if (sameAnnotation(a0, a1, p)) { + return empty(); + } + } + seen.add(a0); + return streamDepthFirst(a0.getAnnotationType().asElement().getAnnotationMirrors(), seen, p); })) .flatMap(identity()); } diff --git a/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java b/src/main/java/org/microbean/construct/element/SyntheticAnnotationTypeElement.java index 202b13d..067d367 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 org.microbean.construct.element.AnnotationMirrors.validAnnotationInterfaceElementScalarType; + /** * An experimental {@link TypeElement} implementation that is wholly synthetic and suitable only for * (partially) modeling {@linkplain AnnotationMirror#getAnnotationType() annotation types}. @@ -420,24 +422,10 @@ public final String toString() { // 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); - } - }; + if (validAnnotationInterfaceElementScalarType(type)) { + return type; } - default -> throw new IllegalArgumentException("type: " + type); - }; + throw new IllegalArgumentException("type: " + type); }