From 9dba3db26a232ba0acc4f909568aadaa3bccad39 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 25 Nov 2024 11:56:19 +0100 Subject: [PATCH] GH-962 - Fix Javadoc extraction for methods referring to types nested in interfaces. A bug [0] in APTK causes our Javadoc extraction to fail for methods whose parameters are classes nested in interfaces. We fix that by temporarily including a fixed copy of TypeElementWrapper, that looks up outer types on interfaces as well. [0] https://github.com/toolisticon/aptk/issues/163 --- .../tools/wrapper/TypeElementWrapper.java | 395 ++++++++++++++++++ .../apt/SpringModulithProcessorUnitTests.java | 8 + .../resources/example/SampleInterface.java | 23 + 3 files changed, 426 insertions(+) create mode 100644 spring-modulith-apt/src/main/java/io/toolisticon/aptk/tools/wrapper/TypeElementWrapper.java create mode 100644 spring-modulith-apt/src/test/resources/example/SampleInterface.java diff --git a/spring-modulith-apt/src/main/java/io/toolisticon/aptk/tools/wrapper/TypeElementWrapper.java b/spring-modulith-apt/src/main/java/io/toolisticon/aptk/tools/wrapper/TypeElementWrapper.java new file mode 100644 index 00000000..f8967bb5 --- /dev/null +++ b/spring-modulith-apt/src/main/java/io/toolisticon/aptk/tools/wrapper/TypeElementWrapper.java @@ -0,0 +1,395 @@ +package io.toolisticon.aptk.tools.wrapper; + +import io.toolisticon.aptk.tools.ElementUtils; +import io.toolisticon.aptk.tools.TypeMirrorWrapper; +import io.toolisticon.aptk.tools.corematcher.AptkCoreMatchers; +import io.toolisticon.aptk.tools.fluentfilter.FluentElementFilter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * Copy of the original {@link TypeElementWrapper} of the Toolisticon APTK project located + * here. Tweaks {@link #getOuterType()} to include + * {@link ElementKind#INTERFACE} in the outer class lookup to fix the issue reported + * here.
+ * Wraps a TypeElement to provide some convenience * functionality + */ +public class TypeElementWrapper extends ElementWrapper { + + private final static String TYPE_ELEMENT_CLASS_NAME = "javax.lang.model.element.TypeElement"; + + /** + * Hidden constructor. + * + * @param typeElement The TypeElement to wrap + */ + private TypeElementWrapper(TypeElement typeElement) { + super(typeElement); + } + + // methods inherited from TypeElement + + /** + * Returns the NestingKind. + * + * @return the NestingKind. + */ + public NestingKind getNestingKind() { + return this.element.getNestingKind(); + } + + public boolean hasNestingKind(NestingKind nestingKind) { + return nestingKind != null && getNestingKind().equals(nestingKind); + } + + public boolean isNested() { + return getNestingKind().isNested(); + } + + /** + * Gets the nested name of the element. If the TypeElement isn't a nested type the simple name will be returned. + * + * @return the nested name of the element + */ + public String getNestedName() { + + String name = getSimpleName(); + + TypeElementWrapper current = this; + + while (current.isNested()) { + + current = current.getOuterType().get(); + name = current.getSimpleName() + "." + name; + + } + + return name; + } + + /** + * Returns the qualified name. + * + * @return the qualified name. + */ + public String getQualifiedName() { + return this.element.getQualifiedName().toString(); + } + + /** + * Check for qualified name. + * + * @param name the name to check for + * @return true if qualified name matches passed name, otherwise false + */ + public boolean hasQualifiedName(String name) { + return name != null && getQualifiedName().equals(name); + } + + /** + * Gets the binary name for the type element. + */ + public String getBinaryName() { + + // According to https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html the binary name looks like: + // -- The binary name of a top level type (§7.6) is its canonical name (§6.7). + // -- The binary name of a member type (§8.5, §9.5) consists of the binary name of its immediately enclosing type, + // followed by $, followed by the simple name of the member. + + if (this.isNested()) { + // Safe to call since its of nested kind + TypeElementWrapper enclosingTypeElement = this.getOuterType().get(); + return enclosingTypeElement.getBinaryName() + "$" + this.getSimpleName(); + } else { + return this.getQualifiedName(); + } + + } + + /** + * Gets the direct superclass + * + * @return a TypeMirrorWrapper of the direct superclass + */ + public TypeMirrorWrapper getSuperclass() { + return TypeMirrorWrapper.wrap(this.element.getSuperclass()); + } + + /** + * Returns list of TypeMirrorWrapper containing all directly implemented interfaces. Doesn't return interfaces + * implemented by superclasses. + * + * @return list of TypeMirrorWrapper containing all directly implemented interfaces + */ + public List getInterfaces() { + return this.element.getInterfaces().stream().map(TypeMirrorWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Returns Set of TypeMirrorWrapper containing all implemented interfaces (including those of superclasses). + * + * @return Set of TypeMirrorWrapper containing all implemented interfaces + */ + public Set getAllInterfaces() { + + // Add own interfaces + Set result = new HashSet<>(this.getInterfaces()); + + // recursively add those of superclasses. + Optional superClass = getSuperclass().getTypeElement(); + superClass.ifPresent(typeElementWrapper -> result.addAll(typeElementWrapper.getAllInterfaces())); + + return result; + } + + /** + * Returns list of TypeParameterElementWrapper instances describing all TypeParameters. + * + * @return list of TypeParameterElementWrapper instances describing all TypeParameters, or an empty list + */ + public List getTypeParameters() { + return this.element.getTypeParameters().stream().map(TypeParameterElementWrapper::wrap) + .collect(Collectors.toList()); + } + + /** + * Checks if wrapped TypeElement has type parameters. + * + * @return true, if TypeElement has type parameters, otherwise false. + */ + public boolean hasTypeParameters() { + return getTypeParameters().size() > 0; + } + + /** + * Returns an enclosed method. Works only if all parameter types are already compiled. + * + * @param name the method name + * @param parameterTypes The parameter types + * @return the wrapped ExecutableElement instance if method is found, otherwise null + */ + public Optional getMethod(String name, Class... parameterTypes) { + List filterResult = FluentElementFilter + .createFluentElementFilter(this.element.getEnclosedElements()) + .applyFilter(AptkCoreMatchers.IS_METHOD) + .applyFilter(AptkCoreMatchers.BY_NAME).filterByOneOf(name) + .applyFilter(AptkCoreMatchers.BY_PARAMETER_TYPE).filterByOneOf(parameterTypes) + .getResult(); + return Optional.ofNullable(filterResult.size() > 0 ? ExecutableElementWrapper.wrap(filterResult.get(0)) : null); + } + + /** + * Returns list of ExecutableElementWrapper for all methods in wrapped TypeElement, filtered by modifiers. + * + * @param modifier modifiers used for filtering + * @return list of ExecutableElementWrapper for all methods in wrapped TypeElement, or empty list + */ + public List getMethods(Modifier... modifier) { + return FluentElementFilter.createFluentElementFilter( + this.element.getEnclosedElements()).applyFilter(AptkCoreMatchers.IS_METHOD) + .applyFilter(AptkCoreMatchers.BY_MODIFIER).filterByAllOf(modifier) + .getResult() + .stream().map(ExecutableElementWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Returns list of VariableElementWrapper for all fields in wrapped TypeElement, filtered by modifiers. + * + * @param modifier modifiers used for filtering + * @return list of VariableElementWrapper for all fields in wrapped TypeElement, or empty list + */ + public List getFields(Modifier... modifier) { + return FluentElementFilter.createFluentElementFilter( + this.element.getEnclosedElements()).applyFilter(AptkCoreMatchers.IS_FIELD) + .applyFilter(AptkCoreMatchers.BY_MODIFIER).filterByAllOf(modifier) + .getResult() + .stream().map(VariableElementWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Returns Optional of VariableElementWrapper of field with passed name in wrapped TypeElement. + * + * @param name Name of the field + * @return Optional of ExecutableElementWrapper of field with passed name in wrapped TypeElement, or empty list + */ + public Optional getFieldByName(String name) { + List fields = FluentElementFilter.createFluentElementFilter( + this.element.getEnclosedElements()).applyFilter(AptkCoreMatchers.IS_FIELD) + .applyFilter(AptkCoreMatchers.BY_NAME).filterByOneOf(name) + .getResult() + .stream().map(VariableElementWrapper::wrap).collect(Collectors.toList()); + + return Optional.ofNullable(fields.size() > 0 ? fields.get(0) : null); + } + + /** + * Returns list of VariableElementWrapper for all constructors in wrapped TypeElement, filtered by modifiers. + * + * @param modifier modifiers used for filtering + * @return list of ExecutableElementWrapper for all constructors in wrapped TypeElement, or empty list + */ + public List getConstructors(Modifier... modifier) { + return FluentElementFilter.createFluentElementFilter( + this.element.getEnclosedElements()).applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR) + .applyFilter(AptkCoreMatchers.BY_MODIFIER).filterByAllOf(modifier) + .getResult() + .stream().map(ExecutableElementWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Returns list of VariableElementWrapper for all inner types in wrapped TypeElement, filtered by modifiers. + * + * @param modifier modifiers used for filtering + * @return list of ExecutableElementWrapper for all inner types in wrapped TypeElement, or empty list + */ + public List getInnerTypes(Modifier... modifier) { + return FluentElementFilter.createFluentElementFilter( + this.element.getEnclosedElements()).applyFilter(AptkCoreMatchers.IS_CLASS) + .applyFilter(AptkCoreMatchers.BY_MODIFIER).filterByAllOf(modifier) + .getResult() + .stream().map(TypeElementWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Returns the direct outer type for nested classes. Returned type may not be a top level type, since java allows + * nested classes in nested classes. + * + * @return an Optional containing the outer type of nested classes, if present. + */ + public Optional getOuterType() { + if (this.element.getNestingKind() != NestingKind.MEMBER) { + return Optional.empty(); + } + + return Optional.of(TypeElementWrapper.wrap(ElementUtils.AccessEnclosingElements + . getFirstEnclosingElementOfKind(this.element, ElementKind.CLASS, ElementKind.INTERFACE))); + } + + /** + * Returns the direct outer type for nested classes. + * + * @return an Optional containing the outer type of nested classes, if present. + */ + public Optional getOuterTopLevelType() { + if (this.element.getNestingKind() != NestingKind.MEMBER) { + return Optional.empty(); + } + + return Optional.of(this.getAllEnclosingElements().stream() + .filter(ElementWrapper::isTypeElement) + .map(ElementWrapper::toTypeElement) + .filter(e -> e.hasNestingKind(NestingKind.TOP_LEVEL)).collect(Collectors.toList()).get(0)); + + } + + /** + * Gets all enum constant names of enum as VariableElements. + * + * @return A list containing all enum constant names or null if wrapped TypeElement is no enum. + */ + public List getEnumValues() { + return isEnum() ? this.filterEnclosedElements() + .applyFilter(AptkCoreMatchers.BY_ELEMENT_KIND) + .filterByOneOf(ElementKind.ENUM_CONSTANT) + .getResult() + .stream().map(e -> VariableElementWrapper.wrap((VariableElement) e)) + .collect(Collectors.toList()) + : null; + } + + /** + * Returns the record components of this class or interface element in declaration order. + * + * @return the record components, or an empty list if there are none + */ + public List getRecordComponents() { + + // This method is available from Java 16 - the introduction of records, so this check is sufficient to prevent + // reflective calling of method + if (!isRecord()) { + return Collections.EMPTY_LIST; + } + List recordComponentElements = this + .> invokeParameterlessMethodOfElement(TYPE_ELEMENT_CLASS_NAME, "getRecordComponents"); + + return recordComponentElements.stream().map(RecordComponentElementWrapper::wrap).collect(Collectors.toList()); + + } + + /** + * Returns the permitted classes of this class or interface element in declaration order. + * + * @return the permitted classes, or an empty list if there are none + */ + public List getPermittedSubclasses() { + + // must make sure that method exists, otherwise return the default value + if (!hasMethod(TYPE_ELEMENT_CLASS_NAME, "getPermittedSubclasses")) { + // TODO MUST CHECK WHAT SHOULD BE RETURNED FOR NON SEALED CLASSES! + return Collections.EMPTY_LIST; + } + + List typeMirrors = this.> invokeParameterlessMethodOfElement(TYPE_ELEMENT_CLASS_NAME, + "getPermittedSubclasses"); + return typeMirrors.stream().map(TypeMirrorWrapper::wrap).collect(Collectors.toList()); + } + + /** + * Wraps a TypeElement. + * + * @param element the TypeElement to wrap + * @return a wrapper instance + */ + public static TypeElementWrapper wrap(TypeElement element) { + return new TypeElementWrapper(element); + } + + /** + * Retrieve TypeElementWrapper by fully qualified name. + * + * @param fqn the fully qualified name of the element + * @return an Optional that contains the wrapper instance, if TypeElement for fqn exists + */ + public static Optional getByFqn(String fqn) { + return getByTypeMirror(TypeMirrorWrapper.wrap(fqn)); + } + + /** + * Retrieve TypeElementWrapper by fully qualified name. + * + * @param clazz the class of the element + * @return an Optional that contains the wrapper instance, if TypeElement for class exists (only for DeclaredTypes) + */ + public static Optional getByClass(Class clazz) { + return getByTypeMirror(TypeMirrorWrapper.wrap(clazz)); + } + + /** + * Retrieve TypeElementWrapper by fully qualified name. + * + * @param typeMirror the TypeMirror of the element + * @return an Optional that contains the wrapper instance, if TypeElement for TypeMirror exists (only for + * DeclaredTypes) + */ + public static Optional getByTypeMirror(TypeMirror typeMirror) { + return getByTypeMirror(TypeMirrorWrapper.wrap(typeMirror)); + } + + public static Optional getByTypeMirror(TypeMirrorWrapper typeMirror) { + return typeMirror.getTypeElement(); + } +} diff --git a/spring-modulith-apt/src/test/java/org/springframework/modulith/apt/SpringModulithProcessorUnitTests.java b/spring-modulith-apt/src/test/java/org/springframework/modulith/apt/SpringModulithProcessorUnitTests.java index 90094423..da8b17b6 100644 --- a/spring-modulith-apt/src/test/java/org/springframework/modulith/apt/SpringModulithProcessorUnitTests.java +++ b/spring-modulith-apt/src/test/java/org/springframework/modulith/apt/SpringModulithProcessorUnitTests.java @@ -63,6 +63,14 @@ class SpringModulithProcessorUnitTests { assertThat(comment).isNotBlank().doesNotContain("\n"); } + @Test // GH-962 + void extractsJavadocFromMethodWithClassNestedInInterfaceAsParameter() { + + assertThatNoException().isThrownBy(() -> { + assertSucceded(getSourceFile("SampleInterface")); + }); + } + private static DoCustomAssertions assertSucceded(String source) { return assertSourceProcessed(source) diff --git a/spring-modulith-apt/src/test/resources/example/SampleInterface.java b/spring-modulith-apt/src/test/resources/example/SampleInterface.java new file mode 100644 index 00000000..38da4dfe --- /dev/null +++ b/spring-modulith-apt/src/test/resources/example/SampleInterface.java @@ -0,0 +1,23 @@ +package example; + +/** + * Interface comment. + * + * @author Oliver Drotbohm + */ +interface SampleInterface { + + /** + * Method with nested class parameter. + * + * @param parameter + */ + void someMethod(SampleClass parameter); + + /** + * Nested class comment. + * + * @author Oliver Drotbohm + */ + class SampleClass {} +}