You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/11 22:16:45 UTC

(maven-plugin-tools) 03/10: MPLUGIN-421: Migrate Javadoc extraction to JavaParser

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch maven-plugin-tools-3.x
in repository https://gitbox.apache.org/repos/asf/maven-plugin-tools.git

commit 61bb93664af9ef44b7fc1c8da1dc1dfc7bf07a6c
Author: Vaclav Haisman <vh...@gmail.com>
AuthorDate: Fri Aug 7 22:22:04 2026 +0200

    MPLUGIN-421: Migrate Javadoc extraction to JavaParser
    
    (cherry picked from commit 8f9d9e966102eaa196ff9cb18d1a7a158e9f8261)
---
 .../JavaAnnotationsMojoDescriptorExtractor.java    | 460 ++++++--------
 .../extractor/annotations/JavaSourceModel.java     |  65 +-
 .../converter/JavaClassConverterContext.java       | 674 +++++++++++----------
 .../converter/JavaClassConverterContextTest.java   |  25 +-
 4 files changed, 599 insertions(+), 625 deletions(-)

diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
index b44f3c37..34357c85 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
@@ -23,9 +23,9 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -42,14 +42,16 @@ import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.stream.Collectors;
 
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
-import com.thoughtworks.qdox.model.DocletTag;
-import com.thoughtworks.qdox.model.JavaAnnotatedElement;
-import com.thoughtworks.qdox.model.JavaClass;
-import com.thoughtworks.qdox.model.JavaField;
-import com.thoughtworks.qdox.model.JavaMember;
-import com.thoughtworks.qdox.model.JavaMethod;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.MethodDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc;
+import com.github.javaparser.ast.type.ClassOrInterfaceType;
+import com.github.javaparser.javadoc.Javadoc;
+import com.github.javaparser.javadoc.JavadocBlockTag;
+import com.github.javaparser.resolution.UnsolvedSymbolException;
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.artifact.versioning.ComparableVersion;
 import org.apache.maven.plugin.descriptor.InvalidParameterException;
@@ -206,9 +208,6 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
                 request.setRequiredJavaVersion(requiredJavaVersion);
             }
         }
-        JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values());
-        Map<String, JavaClass> javaClassesMap = discoverClasses(builder);
-
         final JavadocLinkGenerator linkGenerator;
         if (request.getInternalJavadocBaseUrl() != null
                 || (request.getExternalJavadocBaseUrls() != null
@@ -222,7 +221,12 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
             linkGenerator = null;
         }
 
-        populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
+        try (JavaSourceModel sourceModel = scanJavadoc(request, mojoAnnotatedClasses.values())) {
+            Map<String, TypeDeclaration<?>> javaClassesMap = discoverClasses(sourceModel);
+            populateDataFromJavadoc(sourceModel, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
+        } catch (IOException e) {
+            throw new ExtractionException("Could not parse Java sources: " + e.getMessage(), e);
+        }
 
         return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor());
     }
@@ -242,9 +246,8 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
         return result;
     }
 
-    private JavaProjectBuilder scanJavadoc(
-            PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
-            throws ExtractionException {
+    private JavaSourceModel scanJavadoc(PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
+            throws ExtractionException, IOException {
         // found artifact from reactors to scan sources
         // we currently only scan sources from reactors
         List<MavenProject> mavenProjects = new ArrayList<>();
@@ -252,9 +255,10 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
         // if we need to scan sources from external artifacts
         Set<Artifact> externalArtifacts = new HashSet<>();
 
-        JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
-        builder.setEncoding(request.getEncoding());
-        extendJavaProjectBuilder(request, builder, request.getProject());
+        Charset encoding =
+                request.getEncoding() == null ? StandardCharsets.UTF_8 : Charset.forName(request.getEncoding());
+        JavaSourceModel sourceModel = new JavaSourceModel(encoding);
+        extendJavaSourceModel(request, sourceModel, request.getProject());
 
         for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) {
             if (Objects.equals(
@@ -282,17 +286,18 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
         for (Artifact artifact : externalArtifacts) {
             // parameter for test-sources too ?? olamy I need that for it test only
             if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) {
-                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources");
+                extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "test-sources");
             } else {
-                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources");
+                extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "sources");
             }
         }
 
         for (MavenProject mavenProject : mavenProjects) {
-            extendJavaProjectBuilder(request, builder, mavenProject);
+            extendJavaSourceModel(request, sourceModel, mavenProject);
         }
 
-        return builder;
+        sourceModel.parse();
+        return sourceModel;
     }
 
     private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) {
@@ -303,281 +308,191 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
      * from sources scan to get @since and @deprecated and description of classes and fields.
      */
     protected void populateDataFromJavadoc(
-            JavaProjectBuilder javaProjectBuilder,
+            JavaSourceModel sourceModel,
             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
-            Map<String, JavaClass> javaClassesMap,
+            Map<String, TypeDeclaration<?>> javaClassesMap,
             JavadocLinkGenerator linkGenerator) {
-
         for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) {
-            JavaClass javaClass = javaClassesMap.get(entry.getKey());
+            TypeDeclaration<?> javaClass = javaClassesMap.get(entry.getKey());
             if (javaClass == null) {
                 continue;
             }
-            // populate class-level content
-            MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo();
-            if (mojoAnnotationContent != null) {
+            MojoAnnotationContent mojo = entry.getValue().getMojo();
+            if (mojo != null) {
                 JavaClassConverterContext context = new JavaClassConverterContext(
-                        javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber());
-                mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context));
-
-                DocletTag since = findInClassHierarchy(javaClass, "since");
-                if (since != null) {
-                    mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context));
-                }
-
-                DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated");
-                if (deprecated != null) {
-                    mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
-                }
+                        javaClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber(javaClass));
+                mojo.setDescription(getDescriptionFromElement(javaClass, context));
+                findInClassHierarchy(sourceModel, javaClass, "since")
+                        .ifPresent(tag -> mojo.setSince(getRawValueFromTaglet(tag, context)));
+                findInClassHierarchy(sourceModel, javaClass, "deprecated")
+                        .ifPresent(tag -> mojo.setDeprecated(getRawValueFromTaglet(tag, context)));
             }
 
-            Map<String, JavaAnnotatedElement> fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap);
-            Map<String, JavaAnnotatedElement> methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap);
-
-            // populate parameters
+            Map<String, SourceMember> fields = extractFields(sourceModel, javaClass);
+            Map<String, SourceMember> methods = extractMethods(sourceModel, javaClass);
             Map<String, ParameterAnnotationContent> parameters =
-                    getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses);
-            parameters = new TreeMap<>(parameters);
+                    new TreeMap<>(getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses));
             for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) {
-                JavaAnnotatedElement element;
-                if (parameter.getValue().isAnnotationOnMethod()) {
-                    element = methodsMap.get(parameter.getKey());
-                } else {
-                    element = fieldsMap.get(parameter.getKey());
-                }
-
-                if (element == null) {
-                    continue;
-                }
-
-                JavaClassConverterContext context = new JavaClassConverterContext(
-                        javaClass, ((JavaMember) element).getDeclaringClass(),
-                        javaProjectBuilder, mojoAnnotatedClasses,
-                        linkGenerator, element.getLineNumber());
-                ParameterAnnotationContent parameterAnnotationContent = parameter.getValue();
-                parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context));
-
-                DocletTag deprecated = element.getTagByName("deprecated");
-                if (deprecated != null) {
-                    parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
-                }
-
-                DocletTag since = element.getTagByName("since");
-                if (since != null) {
-                    parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context));
+                SourceMember member = parameter.getValue().isAnnotationOnMethod()
+                        ? methods.get(parameter.getKey())
+                        : fields.get(parameter.getKey());
+                if (member != null) {
+                    populateMemberJavadoc(
+                            javaClass, member, parameter.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
                 }
             }
 
-            // populate components
-            Map<String, ComponentAnnotationContent> components =
-                    entry.getValue().getComponents();
-            for (Map.Entry<String, ComponentAnnotationContent> component : components.entrySet()) {
-                JavaAnnotatedElement element = fieldsMap.get(component.getKey());
-                if (element == null) {
-                    continue;
-                }
-
-                JavaClassConverterContext context = new JavaClassConverterContext(
-                        javaClass, ((JavaMember) element).getDeclaringClass(),
-                        javaProjectBuilder, mojoAnnotatedClasses,
-                        linkGenerator, javaClass.getLineNumber());
-                ComponentAnnotationContent componentAnnotationContent = component.getValue();
-                componentAnnotationContent.setDescription(getDescriptionFromElement(element, context));
-
-                DocletTag deprecated = element.getTagByName("deprecated");
-                if (deprecated != null) {
-                    componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
-                }
-
-                DocletTag since = element.getTagByName("since");
-                if (since != null) {
-                    componentAnnotationContent.setSince(getRawValueFromTaglet(since, context));
+            for (Map.Entry<String, ComponentAnnotationContent> component :
+                    entry.getValue().getComponents().entrySet()) {
+                SourceMember member = fields.get(component.getKey());
+                if (member != null) {
+                    populateMemberJavadoc(
+                            javaClass, member, component.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
                 }
             }
         }
     }
 
-    /**
-     * Returns the XHTML description from the given element.
-     * This may refer to either goal, parameter or component.
-     * @param element the element for which to generate the description
-     * @param context the context with which to call the converter
-     * @return the generated description
-     */
-    String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) {
+    private void populateMemberJavadoc(
+            TypeDeclaration<?> mojoClass,
+            SourceMember member,
+            Object annotation,
+            JavaSourceModel sourceModel,
+            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
+            JavadocLinkGenerator linkGenerator) {
+        JavaClassConverterContext context = new JavaClassConverterContext(
+                mojoClass,
+                member.declaringClass,
+                member.element,
+                sourceModel,
+                mojoAnnotatedClasses,
+                linkGenerator,
+                lineNumber(member.element));
+        String description = getDescriptionFromElement(member.javadocElement, context);
+        Optional<JavadocBlockTag> deprecated = getTag(member.javadocElement, "deprecated");
+        Optional<JavadocBlockTag> since = getTag(member.javadocElement, "since");
+        if (annotation instanceof ParameterAnnotationContent) {
+            ParameterAnnotationContent parameter = (ParameterAnnotationContent) annotation;
+            parameter.setDescription(description);
+            deprecated.ifPresent(tag -> parameter.setDeprecated(getRawValueFromTaglet(tag, context)));
+            since.ifPresent(tag -> parameter.setSince(getRawValueFromTaglet(tag, context)));
+        } else {
+            ComponentAnnotationContent component = (ComponentAnnotationContent) annotation;
+            component.setDescription(description);
+            deprecated.ifPresent(tag -> component.setDeprecated(getRawValueFromTaglet(tag, context)));
+            since.ifPresent(tag -> component.setSince(getRawValueFromTaglet(tag, context)));
+        }
+    }
 
-        String comment = element.getComment();
-        if (comment == null) {
+    String getDescriptionFromElement(NodeWithJavadoc<?> element, JavaClassConverterContext context) {
+        Optional<Javadoc> javadoc = element.getJavadoc();
+        if (!javadoc.isPresent()) {
             return null;
         }
-        StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context));
-        for (DocletTag docletTag : element.getTags()) {
-            // also consider see block tags
-            if ("see".equals(docletTag.getName())) {
-                description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context));
-            }
-        }
+        StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(
+                javadoc.get().getDescription().toText(), context));
+        javadoc.get().getBlockTags().stream()
+                .filter(tag -> "see".equals(tag.getTagName()))
+                .forEach(tag -> description.append(javadocBlockTagsToHtmlConverter.convert(
+                        tag.getTagName(), tag.getContent().toText(), context)));
         return description.toString();
     }
 
-    String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) {
-        // just resolve inline tags and convert to XHTML
-        return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context);
+    String getRawValueFromTaglet(JavadocBlockTag tag, ConverterContext context) {
+        return javadocInlineTagsToHtmlConverter.convert(tag.getContent().toText(), context);
     }
 
-    /**
-     * @param javaClass not null
-     * @param tagName   not null
-     * @return docletTag instance
-     */
-    private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) {
-        try {
-            DocletTag tag = javaClass.getTagByName(tagName);
-
-            if (tag == null) {
-                JavaClass superClass = javaClass.getSuperJavaClass();
-
-                if (superClass != null) {
-                    tag = findInClassHierarchy(superClass, tagName);
-                }
-            }
-
+    private Optional<JavadocBlockTag> findInClassHierarchy(
+            JavaSourceModel sourceModel, TypeDeclaration<?> javaClass, String tagName) {
+        Optional<JavadocBlockTag> tag = getTag(javaClass, tagName);
+        if (tag.isPresent()) {
             return tag;
-        } catch (NoClassDefFoundError e) {
-            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
-                return null;
-            }
-            String str;
-            try {
-                str = javaClass.getFullyQualifiedName();
-            } catch (Throwable t) {
-                str = javaClass.getValue();
-            }
-            LOGGER.warn("Failed extracting tag '" + tagName + "' from class " + str);
-            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
         }
+        return getSuperSourceClass(sourceModel, javaClass)
+                .flatMap(parent -> findInClassHierarchy(sourceModel, parent, tagName));
     }
 
-    /**
-     * extract fields that are either parameters or components.
-     *
-     * @param javaClass not null
-     * @return map with Mojo parameters names as keys
-     */
-    private Map<String, JavaAnnotatedElement> extractFieldsAnnotations(
-            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
-        try {
-            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
-
-            // we have to add the parent fields first, so that they will be overwritten by the local fields if
-            // that actually happens...
-            JavaClass superClass = javaClass.getSuperJavaClass();
-
-            if (superClass != null) {
-                if (!superClass.getFields().isEmpty()) {
-                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
-                }
-                // maybe sources comes from scan of sources artifact
-                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
-                if (superClass != null && !superClass.getFields().isEmpty()) {
-                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
-                }
-            } else {
-
-                rawParams = new TreeMap<>();
-            }
-
-            for (JavaField field : javaClass.getFields()) {
-                rawParams.put(field.getName(), field);
-            }
+    private static Optional<JavadocBlockTag> getTag(NodeWithJavadoc<?> element, String tagName) {
+        return element.getJavadoc()
+                .flatMap(javadoc -> javadoc.getBlockTags().stream()
+                        .filter(tag -> tagName.equals(tag.getTagName()))
+                        .findFirst());
+    }
 
-            return rawParams;
-        } catch (NoClassDefFoundError e) {
-            LOGGER.warn("Failed extracting parameters from " + javaClass);
-            throw e;
+    private Map<String, SourceMember> extractFields(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+        Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
+                .map(parent -> extractFields(sourceModel, parent))
+                .orElseGet(TreeMap::new);
+        for (FieldDeclaration field : javaClass.getFields()) {
+            field.getVariables()
+                    .forEach(variable ->
+                            result.put(variable.getNameAsString(), new SourceMember(javaClass, field, field)));
         }
+        return result;
     }
 
-    /**
-     * extract methods that are parameters.
-     *
-     * @param javaClass not null
-     * @return map with Mojo parameters names as keys
-     */
-    private Map<String, JavaAnnotatedElement> extractMethodsAnnotations(
-            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
-        try {
-            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
-
-            // we have to add the parent methods first, so that they will be overwritten by the local methods if
-            // that actually happens...
-            JavaClass superClass = javaClass.getSuperJavaClass();
-
-            if (superClass != null) {
-                if (!superClass.getMethods().isEmpty()) {
-                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
-                }
-                // maybe sources comes from scan of sources artifact
-                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
-                if (superClass != null && !superClass.getMethods().isEmpty()) {
-                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
-                }
-            } else {
-
-                rawParams = new TreeMap<>();
-            }
-
-            for (JavaMethod method : javaClass.getMethods()) {
-                if (isPublicSetterMethod(method)) {
-                    rawParams.put(
-                            StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method);
-                }
-            }
-
-            return rawParams;
-        } catch (NoClassDefFoundError e) {
-            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
-                return new TreeMap<>();
-            }
-            String str;
-            try {
-                str = javaClass.getFullyQualifiedName();
-            } catch (Throwable t) {
-                str = javaClass.getValue();
+    private Map<String, SourceMember> extractMethods(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+        Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
+                .map(parent -> extractMethods(sourceModel, parent))
+                .orElseGet(TreeMap::new);
+        for (MethodDeclaration method : javaClass.getMethods()) {
+            if (isPublicSetterMethod(method)) {
+                result.put(
+                        StringUtils.lowercaseFirstLetter(
+                                method.getNameAsString().substring(3)),
+                        new SourceMember(javaClass, method, method));
             }
-            LOGGER.warn("Failed extracting methods from " + str);
-            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
         }
+        return result;
     }
 
-    private boolean isPublicSetterMethod(JavaMethod method) {
+    private static boolean isPublicSetterMethod(MethodDeclaration method) {
         return method.isPublic()
                 && !method.isStatic()
-                && method.getName().length() > 3
-                && (method.getName().startsWith("add") || method.getName().startsWith("set"))
-                && "void".equals(method.getReturnType().getValue())
+                && method.getNameAsString().length() > 3
+                && (method.getNameAsString().startsWith("add")
+                        || method.getNameAsString().startsWith("set"))
+                && method.getType().isVoidType()
                 && method.getParameters().size() == 1;
     }
 
-    protected Map<String, JavaClass> discoverClasses(JavaProjectBuilder builder) {
-        Collection<JavaClass> javaClasses = builder.getClasses();
-
-        if (javaClasses == null || javaClasses.size() < 1) {
-            return Collections.emptyMap();
+    private Optional<TypeDeclaration<?>> getSuperSourceClass(
+            JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+        if (!(javaClass instanceof ClassOrInterfaceDeclaration)
+                || ((ClassOrInterfaceDeclaration) javaClass).isInterface()) {
+            return Optional.empty();
         }
-
-        Map<String, JavaClass> javaClassMap = new HashMap<>(javaClasses.size());
-
-        for (JavaClass javaClass : javaClasses) {
-            javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
+        Optional<ClassOrInterfaceType> superClass = ((ClassOrInterfaceDeclaration) javaClass)
+                .getExtendedTypes().stream().findFirst();
+        if (!superClass.isPresent()) {
+            return Optional.empty();
+        }
+        try {
+            return superClass
+                    .get()
+                    .resolve()
+                    .asReferenceType()
+                    .getTypeDeclaration()
+                    .flatMap(sourceModel::getType);
+        } catch (UnsolvedSymbolException e) {
+            // Sources artifacts need not contain the sources of every superclass. In that case there is no
+            // source Javadoc to inherit, just as when no sources artifact is available at all.
+            LOGGER.debug("Could not resolve source superclass {}", superClass.get(), e);
+            return Optional.empty();
         }
+    }
 
-        return javaClassMap;
+    protected Map<String, TypeDeclaration<?>> discoverClasses(JavaSourceModel sourceModel) {
+        Map<String, TypeDeclaration<?>> result = new HashMap<>();
+        for (TypeDeclaration<?> type : sourceModel.getTypes()) {
+            type.getFullyQualifiedName().ifPresent(name -> result.put(name, type));
+        }
+        return result;
     }
 
-    protected void extendJavaProjectBuilderWithSourcesJar(
-            JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier)
-            throws ExtractionException {
+    protected void extendJavaSourceModelWithSourcesJar(
+            JavaSourceModel sourceModel, Artifact artifact, PluginToolsRequest request, String classifier)
+            throws ExtractionException, IOException {
         try {
             org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact(
                     artifact.getGroupId(),
@@ -621,17 +536,17 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
                 unArchiver.setDestDirectory(extractDirectory);
                 unArchiver.extract();
 
-                extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies());
+                extendJavaSourceModel(sourceModel, Arrays.asList(extractDirectory), request.getDependencies());
             } else if (sourcesArtifact.getFile().isDirectory()) {
-                extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
+                extendJavaSourceModel(sourceModel, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
             }
         } catch (ArchiverException | NoSuchArchiverException e) {
             throw new ExtractionException(e.getMessage(), e);
         }
     }
 
-    private void extendJavaProjectBuilder(
-            PluginToolsRequest request, JavaProjectBuilder builder, final MavenProject project) {
+    private void extendJavaSourceModel(
+            PluginToolsRequest request, JavaSourceModel sourceModel, final MavenProject project) throws IOException {
         List<File> sources = new ArrayList<>();
 
         for (String source : project.getCompileSourceRoots()) {
@@ -651,25 +566,32 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
             sources.add(generatedPlugin);
         }
 
-        extendJavaProjectBuilder(builder, sources, project.getArtifacts());
+        extendJavaSourceModel(sourceModel, sources, project.getArtifacts());
     }
 
-    private void extendJavaProjectBuilder(
-            JavaProjectBuilder builder, List<File> sourceDirectories, Set<Artifact> artifacts) {
-
-        // Build isolated Classloader with only the artifacts of the project (none of this plugin)
-        List<URL> urls = new ArrayList<>(artifacts.size());
+    private void extendJavaSourceModel(
+            JavaSourceModel sourceModel, List<File> sourceDirectories, Set<Artifact> artifacts) throws IOException {
         for (Artifact artifact : artifacts) {
-            try {
-                urls.add(artifact.getFile().toURI().toURL());
-            } catch (MalformedURLException e) {
-                // noop
-            }
+            sourceModel.addClassPathEntry(artifact.getFile());
         }
-        builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));
-
         for (File source : sourceDirectories) {
-            builder.addSourceTree(source);
+            sourceModel.addSourceDirectory(source);
+        }
+    }
+
+    private static int lineNumber(Node node) {
+        return node.getBegin().map(position -> position.line).orElse(0);
+    }
+
+    private static final class SourceMember {
+        private final TypeDeclaration<?> declaringClass;
+        private final Node element;
+        private final NodeWithJavadoc<?> javadocElement;
+
+        private SourceMember(TypeDeclaration<?> declaringClass, Node element, NodeWithJavadoc<?> javadocElement) {
+            this.declaringClass = declaringClass;
+            this.element = element;
+            this.javadocElement = javadocElement;
         }
     }
 
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
index 1d6562c7..ac1e8a49 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
@@ -56,7 +56,7 @@ import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeS
 import com.github.javaparser.utils.SourceRoot;
 
 /** Source declarations and type resolution used while extracting Javadocs. */
-final class JavaSourceModel implements Closeable {
+public final class JavaSourceModel implements Closeable {
 
     private final Charset encoding;
     private final Set<Path> sourceDirectories = new LinkedHashSet<>();
@@ -64,29 +64,30 @@ final class JavaSourceModel implements Closeable {
     private final Map<String, TypeDeclaration<?>> types = new LinkedHashMap<>();
     private final Map<String, String> modulesByExportedPackage = new LinkedHashMap<>();
     private final Set<String> packages = new LinkedHashSet<>();
+    private final Set<String> internalPackages = new LinkedHashSet<>();
 
     private ParserConfiguration parserConfiguration;
     private CombinedTypeSolver typeSolver;
     private URLClassLoader classPathLoader;
     private boolean parsed;
 
-    JavaSourceModel(Charset encoding) {
+    public JavaSourceModel(Charset encoding) {
         this.encoding = encoding;
     }
 
-    void addSourceDirectory(File directory) throws IOException {
+    public void addSourceDirectory(File directory) throws IOException {
         if (directory != null && directory.isDirectory()) {
             sourceDirectories.add(directory.toPath().toRealPath());
         }
     }
 
-    void addClassPathEntry(File entry) throws IOException {
+    public void addClassPathEntry(File entry) throws IOException {
         if (entry != null && entry.exists()) {
             classPathEntries.add(entry.toPath().toRealPath());
         }
     }
 
-    void parse() throws IOException {
+    public void parse() throws IOException {
         if (parsed) {
             return;
         }
@@ -132,8 +133,9 @@ final class JavaSourceModel implements Closeable {
                             .collect(Collectors.joining(System.lineSeparator()));
                     throw new IOException("Unable to parse " + path + System.lineSeparator() + problems);
                 }
-                CompilationUnit unit = result.getResult().orElseThrow(
-                        () -> new IOException("Parser returned no compilation unit for " + result.getSourcePath()));
+                CompilationUnit unit = result.getResult()
+                        .orElseThrow(() ->
+                                new IOException("Parser returned no compilation unit for " + result.getSourcePath()));
                 index(unit);
             }
         }
@@ -156,57 +158,72 @@ final class JavaSourceModel implements Closeable {
     private void index(CompilationUnit unit) {
         unit.getPackageDeclaration()
                 .map(declaration -> declaration.getName().asString())
-                .ifPresent(packages::add);
+                .ifPresent(name -> {
+                    packages.add(name);
+                    internalPackages.add(name);
+                });
         for (TypeDeclaration<?> type : unit.findAll(TypeDeclaration.class)) {
             type.getFullyQualifiedName().ifPresent(name -> types.putIfAbsent(name, type));
         }
-        unit.getModule().ifPresent(module -> module.getDirectives().stream()
-                .filter(ModuleExportsDirective.class::isInstance)
-                .map(ModuleExportsDirective.class::cast)
-                .forEach(exports -> modulesByExportedPackage.putIfAbsent(
-                        exports.getName().asString(), module.getName().asString())));
+        unit.getModule()
+                .ifPresent(module -> module.getDirectives().stream()
+                        .filter(ModuleExportsDirective.class::isInstance)
+                        .map(ModuleExportsDirective.class::cast)
+                        .forEach(exports -> modulesByExportedPackage.putIfAbsent(
+                                exports.getName().asString(), module.getName().asString())));
     }
 
-    Collection<TypeDeclaration<?>> getTypes() {
+    public Collection<TypeDeclaration<?>> getTypes() {
         return Collections.unmodifiableCollection(types.values());
     }
 
-    Optional<TypeDeclaration<?>> getType(String fullyQualifiedName) {
+    public Optional<TypeDeclaration<?>> getType(String fullyQualifiedName) {
         return Optional.ofNullable(types.get(fullyQualifiedName));
     }
 
-    Optional<TypeDeclaration<?>> getType(ResolvedReferenceTypeDeclaration declaration) {
+    public Optional<TypeDeclaration<?>> getType(ResolvedReferenceTypeDeclaration declaration) {
         return getType(declaration.getQualifiedName());
     }
 
-    Optional<String> getModuleName(String packageName) {
+    public Optional<String> getModuleName(String packageName) {
         return Optional.ofNullable(modulesByExportedPackage.get(packageName));
     }
 
-    boolean hasPackage(String packageName) {
+    public boolean hasPackage(String packageName) {
         return packages.contains(packageName) || Package.getPackage(packageName) != null;
     }
 
-    boolean isInternal(ResolvedReferenceTypeDeclaration declaration) {
+    public boolean isInternal(ResolvedReferenceTypeDeclaration declaration) {
         return types.containsKey(declaration.getQualifiedName());
     }
 
-    Optional<ResolvedReferenceTypeDeclaration> resolveType(String fullyQualifiedName) {
+    public boolean isInternalPackage(String packageName) {
+        return internalPackages.contains(packageName);
+    }
+
+    public Optional<ResolvedReferenceTypeDeclaration> resolveType(String fullyQualifiedName) {
         ensureParsed();
         SymbolReference<ResolvedReferenceTypeDeclaration> reference = typeSolver.tryToSolveType(fullyQualifiedName);
         return reference.isSolved() ? Optional.of(reference.getCorrespondingDeclaration()) : Optional.empty();
     }
 
-    TypeSolver getTypeSolver() {
+    public TypeSolver getTypeSolver() {
         ensureParsed();
         return typeSolver;
     }
 
-    String getLocation(Node node, int fallbackLine) {
-        int line = node.getBegin().map(position -> position.line).orElse(fallbackLine);
+    public String getLocation(Node node, int fallbackLine) {
+        int line = fallbackLine > 0
+                ? fallbackLine
+                : node.getBegin().map(position -> position.line).orElse(0);
         return node.findCompilationUnit()
                 .flatMap(CompilationUnit::getStorage)
-                .map(storage -> storage.getPath().toString() + ":" + line)
+                .map(storage -> java.nio.file.Paths.get("")
+                                .toAbsolutePath()
+                                .toUri()
+                                .relativize(storage.getPath().toUri())
+                                .toString()
+                        + ":" + line)
                 .orElse("unknown:" + line);
     }
 
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
index b039d5b4..bc6bf04f 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
@@ -20,8 +20,6 @@ package org.apache.maven.tools.plugin.extractor.annotations.converter;
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -30,121 +28,101 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.builder.TypeAssembler;
-import com.thoughtworks.qdox.library.ClassNameLibrary;
-import com.thoughtworks.qdox.model.JavaClass;
-import com.thoughtworks.qdox.model.JavaField;
-import com.thoughtworks.qdox.model.JavaModule;
-import com.thoughtworks.qdox.model.JavaPackage;
-import com.thoughtworks.qdox.model.JavaType;
-import com.thoughtworks.qdox.parser.structs.TypeDef;
-import com.thoughtworks.qdox.type.TypeResolver;
+import com.github.javaparser.ast.ImportDeclaration;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
+import com.github.javaparser.resolution.types.ResolvedReferenceType;
+import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
 import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
 import org.apache.maven.tools.plugin.javadoc.JavadocReference;
 
-/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */
+/** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */
 public class JavaClassConverterContext implements ConverterContext {
 
-    final JavaClass mojoClass; // this is the mojo's class
+    /** The Mojo class whose documentation is being generated. */
+    private final TypeDeclaration<?> mojoClass;
 
-    final JavaClass declaringClass; // this may be a super class of the mojo's class
+    /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */
+    private final TypeDeclaration<?> declaringClass;
 
-    final JavaProjectBuilder javaProjectBuilder;
+    private final Node locationNode;
+    private final JavaSourceModel sourceModel;
+    private final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
 
-    final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
+    /** The link generator, or {@code null} when no Javadoc site is configured. */
+    private final JavadocLinkGenerator linkGenerator;
 
-    final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured
-
-    final int lineNumber;
-
-    final Optional<JavaModule> javaModule;
-
-    final Map<String, Object> attributes;
+    private final int lineNumber;
+    private final Map<String, Object> attributes = new HashMap<>();
 
     public JavaClassConverterContext(
-            JavaClass mojoClass,
-            JavaProjectBuilder javaProjectBuilder,
+            TypeDeclaration<?> mojoClass,
+            JavaSourceModel sourceModel,
             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
             JavadocLinkGenerator linkGenerator,
             int lineNumber) {
-        this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber);
+        this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber);
     }
 
     public JavaClassConverterContext(
-            JavaClass mojoClass,
-            JavaClass declaringClass,
-            JavaProjectBuilder javaProjectBuilder,
+            TypeDeclaration<?> mojoClass,
+            TypeDeclaration<?> declaringClass,
+            Node locationNode,
+            JavaSourceModel sourceModel,
             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
             JavadocLinkGenerator linkGenerator,
             int lineNumber) {
         this.mojoClass = mojoClass;
         this.declaringClass = declaringClass;
-        this.javaProjectBuilder = javaProjectBuilder;
+        this.locationNode = locationNode;
+        this.sourceModel = sourceModel;
         this.mojoAnnotatedClasses = mojoAnnotatedClasses;
         this.linkGenerator = linkGenerator;
         this.lineNumber = lineNumber;
-        this.attributes = new HashMap<>();
-
-        javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream()
-                .filter(m -> m.getDescriptor().getExports().stream()
-                        .anyMatch(e -> e.getSource().getName().equals(getPackageName())))
-                .findFirst();
     }
 
     @Override
     public Optional<String> getModuleName() {
-        // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed
-        return javaModule.map(JavaModule::getName);
+        return sourceModel.getModuleName(getPackageName());
     }
 
     @Override
     public String getPackageName() {
-        return mojoClass.getPackageName();
+        return resolve(mojoClass).getPackageName();
     }
 
     @Override
     public String getLocation() {
-        try {
-            URL url = declaringClass.getSource().getURL();
-            if (url == null) // url is not always available, just emit FQCN in that case
-            {
-                return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber;
-            }
-            return Paths.get("").toUri().relativize(url.toURI()) + ":" + lineNumber;
-        } catch (URISyntaxException e) {
-            return declaringClass.getSource().getURL() + ":" + lineNumber;
-        }
+        return sourceModel.getLocation(locationNode, lineNumber);
     }
 
-    /**
-     * @param reference
-     * @return true in case either the current context class or any of its super classes are referenced
-     */
+    /** Returns whether {@code reference} identifies the Mojo class or one of its ancestors. */
     @Override
     public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
-        JavaClass javaClassInHierarchy = this.mojoClass;
-        while (javaClassInHierarchy != null) {
-            if (isClassReferencedByReference(javaClassInHierarchy, reference)) {
-                return true;
-            }
-            // check implemented interfaces
-            for (JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces()) {
-                if (isClassReferencedByReference(implementedInterfaces, reference)) {
-                    return true;
-                }
-            }
-            javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass();
+        ResolvedReferenceTypeDeclaration declaration = resolve(mojoClass);
+        if (isClassReferencedByReference(declaration, reference)) {
+            return true;
         }
-        return false;
+        return declaration.getAllAncestors().stream()
+                .map(ResolvedReferenceType::getTypeDeclaration)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .anyMatch(type -> isClassReferencedByReference(type, reference));
     }
 
-    private static boolean isClassReferencedByReference(JavaClass javaClass, FullyQualifiedJavadocReference reference) {
-        return javaClass.getPackageName().equals(reference.getPackageName().orElse(""))
-                && javaClass.getSimpleName().equals(reference.getClassName().orElse(""));
+    private static boolean isClassReferencedByReference(
+            ResolvedReferenceTypeDeclaration declaration, FullyQualifiedJavadocReference reference) {
+        return declaration.getPackageName().equals(reference.getPackageName().orElse(""))
+                && declaration.getClassName().equals(reference.getClassName().orElse(""));
     }
 
     @Override
@@ -157,8 +135,8 @@ public class JavaClassConverterContext implements ConverterContext {
         try {
             if (isReferencedBy(reference)
                     && MemberType.FIELD == reference.getMemberType().orElse(null)) {
-                // link to current goal's parameters
-                return new URI(null, null, reference.getMember().orElse(null)); // just an anchor if same context
+                // Fields in the current Mojo link to parameter anchors on the same page.
+                return new URI(null, null, reference.getMember().orElse(null));
             }
             Optional<String> fqClassName = reference.getFullyQualifiedClassName();
             if (fqClassName.isPresent()) {
@@ -167,7 +145,7 @@ public class JavaClassConverterContext implements ConverterContext {
                         && mojoAnnotatedClass.getMojo() != null
                         && (!reference.getLabel().isPresent()
                                 || MemberType.FIELD == reference.getMemberType().orElse(null))) {
-                    // link to other mojo (only for fields = parameters or without member)
+                    // Fields and whole-class references to another Mojo link to that Mojo's page.
                     return new URI(
                             null,
                             "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
@@ -175,7 +153,7 @@ public class JavaClassConverterContext implements ConverterContext {
                 }
             }
         } catch (URISyntaxException e) {
-            throw new IllegalStateException("Error constructing a valid URL", e); // should not happen
+            throw new IllegalStateException("Error constructing a valid URL", e);
         }
         if (linkGenerator == null) {
             throw new IllegalStateException("No Javadoc Sites given to create URLs to");
@@ -185,296 +163,352 @@ public class JavaClassConverterContext implements ConverterContext {
 
     @Override
     public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
-        Optional<FullyQualifiedJavadocReference> resolvedName;
-        // is it already fully qualified?
+        Optional<FullyQualifiedJavadocReference> resolved;
+        // First try the reference exactly as written, which handles fully qualified names.
         if (reference.getPackageNameClassName().isPresent()) {
-            resolvedName = resolveMember(
-                    reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel());
-            if (resolvedName.isPresent()) {
-                return resolvedName.get();
+            String name = reference.getPackageNameClassName().get();
+            resolved = resolveNamedReference(name, reference.getMember(), reference.getLabel());
+            if (resolved.isPresent()) {
+                return resolved.get();
             }
         }
-        // is it a member only?
+
+        // Search member-only references in the current class or interface first, then its ancestors.
         if (reference.getMember().isPresent()
                 && !reference.getPackageNameClassName().isPresent()) {
-            // search order for not fully qualified names:
-            // 1. The current class or interface (only for members)
-            resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel());
-            if (resolvedName.isPresent()) {
-                return resolvedName.get();
-            }
-            // 2. Any enclosing classes and interfaces searching the closest first (only members)
-            for (JavaClass nestedClass : declaringClass.getNestedClasses()) {
-                resolvedName = resolveMember(nestedClass, reference.getMember(), reference.getLabel());
-                if (resolvedName.isPresent()) {
-                    return resolvedName.get();
-                }
+            resolved = resolveMember(resolve(declaringClass), reference.getMember(), reference.getLabel(), true);
+            if (resolved.isPresent()) {
+                return resolved.get();
             }
-            // 3. Any superclasses and superinterfaces, searching the closest first. (only members)
-            JavaClass superClass = declaringClass.getSuperJavaClass();
-            while (superClass != null) {
-                resolvedName = resolveMember(superClass, reference.getMember(), reference.getLabel());
-                if (resolvedName.isPresent()) {
-                    return resolvedName.get();
-                }
-                superClass = superClass.getSuperJavaClass();
-            }
-        } else {
-            String packageNameClassName = reference.getPackageNameClassName().get();
-            // 4. The current package
-            resolvedName = resolveMember(
-                    declaringClass.getPackageName() + "." + packageNameClassName,
-                    reference.getMember(),
-                    reference.getLabel());
-            if (resolvedName.isPresent()) {
-                return resolvedName.get();
-            }
-            // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement.
-            List<String> importNames = new ArrayList<>();
-            importNames.add("java.lang.*"); // default import
-            importNames.addAll(declaringClass.getSource().getImports());
-            for (String importName : importNames) {
-                if (importName.endsWith(".*")) {
-                    resolvedName = resolveMember(
-                            importName.replace("*", packageNameClassName), reference.getMember(), reference.getLabel());
-                    if (resolvedName.isPresent()) {
-                        return resolvedName.get();
-                    }
-                } else {
-                    if (importName.endsWith(packageNameClassName)) {
-                        resolvedName = resolveMember(importName, reference.getMember(), reference.getLabel());
-                        if (resolvedName.isPresent()) {
-                            return resolvedName.get();
-                        }
-                    } else {
-                        // ends with prefix of reference (nested class name)
-                        int firstDotIndex = packageNameClassName.indexOf(".");
-                        if (firstDotIndex > 0
-                                && importName.endsWith(packageNameClassName.substring(0, firstDotIndex))) {
-                            resolvedName = resolveMember(
-                                    importName,
-                                    packageNameClassName.substring(firstDotIndex + 1),
-                                    reference.getMember(),
-                                    reference.getLabel());
-                            if (resolvedName.isPresent()) {
-                                return resolvedName.get();
-                            }
-                        }
-                    }
+        } else if (reference.getPackageNameClassName().isPresent()) {
+            // Resolve simple and nested type names using Javadoc's lookup order.
+            String name = reference.getPackageNameClassName().get();
+            for (String candidate : classNameCandidates(name)) {
+                resolved = resolveNamedReference(candidate, reference.getMember(), reference.getLabel());
+                if (resolved.isPresent()) {
+                    return resolved.get();
                 }
             }
         }
         throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
     }
 
-    @Override
-    public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
-        String fqcn = reference
-                .getFullyQualifiedClassName()
-                .orElseThrow(() ->
-                        new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
-        String fieldName = reference
-                .getMember()
-                .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
-        JavaClass javaClass = javaProjectBuilder.getClassByName(fqcn);
-        JavaField javaField = javaClass.getFieldByName(fieldName);
-        if (javaField == null) {
-            throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
+    private Optional<FullyQualifiedJavadocReference> resolveNamedReference(
+            String name, Optional<String> member, Optional<String> label) {
+        Optional<ResolvedReferenceTypeDeclaration> type = sourceModel.resolveType(name);
+        if (type.isPresent()) {
+            return resolveMember(type.get(), member, label, true);
         }
-        if (!javaField.isStatic()) {
-            throw new IllegalArgumentException("Field with name " + fieldName + " in class " + fqcn + " is not static");
+        // Package references cannot contain members.
+        if (!member.isPresent() && sourceModel.hasPackage(name)) {
+            return Optional.of(new FullyQualifiedJavadocReference(name, label, !sourceModel.isInternalPackage(name)));
         }
-        return javaField.getInitializationExpression();
+        return Optional.empty();
     }
 
-    @Override
-    public URI getInternalJavadocSiteBaseUrl() {
-        return linkGenerator.getInternalJavadocSiteBaseUrl();
-    }
-
-    private Optional<FullyQualifiedJavadocReference> resolveMember(
-            String fullyQualifiedPackageNameClassName, Optional<String> member, Optional<String> label) {
-        return resolveMember(fullyQualifiedPackageNameClassName, "", member, label);
+    private List<String> classNameCandidates(String name) {
+        List<String> candidates = new ArrayList<>();
+        ResolvedReferenceTypeDeclaration declaration = resolve(declaringClass);
+        // Search order: current package, implicit java.lang import, then explicit imports in declaration order.
+        candidates.add(declaration.getPackageName() + "." + name);
+        candidates.add("java.lang." + name);
+        List<ImportDeclaration> imports = declaringClass
+                .findCompilationUnit()
+                .<List<ImportDeclaration>>map(unit -> new ArrayList<>(unit.getImports()))
+                .orElseGet(Collections::emptyList);
+        for (ImportDeclaration importDeclaration : imports) {
+            if (importDeclaration.isStatic()) {
+                continue;
+            }
+            String importName = importDeclaration.getNameAsString();
+            if (importDeclaration.isAsterisk()) {
+                candidates.add(importName + "." + name);
+            } else if (name.equals(simpleName(importName))) {
+                candidates.add(importName);
+            } else if (name.startsWith(simpleName(importName) + ".")) {
+                // An imported outer type may prefix a nested-type reference.
+                candidates.add(
+                        importName + name.substring(simpleName(importName).length()));
+            }
+        }
+        return candidates;
     }
 
     private Optional<FullyQualifiedJavadocReference> resolveMember(
-            String fullyQualifiedPackageNameClassName,
-            String nestedClassName,
+            ResolvedReferenceTypeDeclaration type,
             Optional<String> member,
-            Optional<String> label) {
-        JavaClass javaClass = javaProjectBuilder.getClassByName(fullyQualifiedPackageNameClassName);
-        if (!isClassFound(javaClass)) {
-            JavaPackage javaPackage = javaProjectBuilder.getPackageByName(fullyQualifiedPackageNameClassName);
-            if (javaPackage == null || !nestedClassName.isEmpty()) {
-                // is it a nested class?
-                int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf('.');
-                if (lastIndexOfDot > 0) {
-                    String newNestedClassName = nestedClassName;
-                    if (!newNestedClassName.isEmpty()) {
-                        newNestedClassName += '.';
-                    }
-                    newNestedClassName += fullyQualifiedPackageNameClassName.substring(lastIndexOfDot + 1);
-                    return resolveMember(
-                            fullyQualifiedPackageNameClassName.substring(0, lastIndexOfDot),
-                            newNestedClassName,
-                            member,
-                            label);
-                }
-                return Optional.empty();
-            } else {
-                // reference to java package never has a member
-                return Optional.of(
-                        new FullyQualifiedJavadocReference(javaPackage.getName(), label, isExternal(javaPackage)));
-            }
-        } else {
-            if (!nestedClassName.isEmpty()) {
-                javaClass = javaClass.getNestedClassByName(nestedClassName);
-                if (javaClass == null) {
-                    return Optional.empty();
-                }
-            }
+            Optional<String> label,
+            boolean includeAncestors) {
+        if (!member.isPresent()) {
+            return Optional.of(toReference(type, Optional.empty(), Optional.empty(), label));
+        }
+
+        String memberText = member.get();
+        // Resolve ambiguous member text as a field, method, then constructor.
+        Optional<ResolvedFieldDeclaration> field = findField(type, memberText, includeAncestors);
+        if (field.isPresent()) {
+            return Optional.of(toReference(
+                    field.get().declaringType().asReferenceType(), member, Optional.of(MemberType.FIELD), label));
+        }
 
-            return resolveMember(javaClass, member, label);
+        String methodName = methodName(memberText);
+        Optional<List<String>> parameterTypes = parameterTypes(memberText);
+        Optional<ResolvedMethodDeclaration> method = findMethod(type, methodName, parameterTypes, includeAncestors);
+        if (method.isPresent()) {
+            return Optional.of(toReference(
+                    method.get().declaringType(),
+                    Optional.of(canonicalMember(method.get())),
+                    Optional.of(MemberType.METHOD),
+                    label));
+        }
+
+        if (methodName.equals(type.getName())) {
+            Optional<ResolvedConstructorDeclaration> constructor = findConstructor(type, parameterTypes);
+            if (constructor.isPresent()) {
+                return Optional.of(toReference(
+                        type,
+                        Optional.of(canonicalMember(constructor.get())),
+                        Optional.of(MemberType.CONSTRUCTOR),
+                        label));
+            }
         }
+        return Optional.empty();
     }
 
-    private boolean isExternal(JavaClass javaClass) {
-        return isExternal(javaClass.getPackage());
+    private Optional<ResolvedFieldDeclaration> findField(
+            ResolvedReferenceTypeDeclaration type, String name, boolean includeAncestors) {
+        List<ResolvedFieldDeclaration> fields = includeAncestors ? type.getAllFields() : type.getDeclaredFields();
+        return fields.stream().filter(field -> field.getName().equals(name)).findFirst();
     }
 
-    private boolean isExternal(JavaPackage javaPackage) {
-        return !javaPackage.getJavaClassLibrary().equals(mojoClass.getJavaClassLibrary());
+    private Optional<ResolvedMethodDeclaration> findMethod(
+            ResolvedReferenceTypeDeclaration type,
+            String name,
+            Optional<List<String>> parameterTypes,
+            boolean includeAncestors) {
+        List<ResolvedReferenceTypeDeclaration> hierarchy = new ArrayList<>();
+        hierarchy.add(type);
+        if (includeAncestors) {
+            type.getAllAncestors().stream()
+                    .map(ResolvedReferenceType::getTypeDeclaration)
+                    .filter(Optional::isPresent)
+                    .map(Optional::get)
+                    .forEach(hierarchy::add);
+        }
+        for (ResolvedReferenceTypeDeclaration declaration : hierarchy) {
+            Optional<ResolvedMethodDeclaration> method = declaration.getDeclaredMethods().stream()
+                    .filter(candidate -> candidate.getName().equals(name))
+                    .filter(candidate -> matches(candidate, parameterTypes))
+                    .findFirst();
+            if (method.isPresent()) {
+                return method;
+            }
+        }
+        return Optional.empty();
     }
 
-    private Optional<FullyQualifiedJavadocReference> resolveMember(
-            JavaClass javaClass, Optional<String> member, Optional<String> label) {
-        final Optional<MemberType> memberType;
-        Optional<String> resolvedMember = member;
-        if (member.isPresent()) {
-            // member is either field...
-            if (javaClass.getFieldByName(member.get()) == null) {
-                // ...is method...
-                List<JavaType> parameterTypes = getParameterTypes(member.get());
-                String methodName = getMethodName(member.get());
-                if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) {
-                    // ...or is constructor
-                    if ((!methodName.equals(javaClass.getSimpleName()))
-                            || (javaClass.getConstructor(parameterTypes) == null)) {
-                        return Optional.empty();
-                    } else {
-                        memberType = Optional.of(MemberType.CONSTRUCTOR);
-                    }
-                } else {
-                    memberType = Optional.of(MemberType.METHOD);
-                }
-                // reconstruct member with fully qualified names but leaving out the argument names
-                StringBuilder memberBuilder = new StringBuilder(methodName);
-                memberBuilder.append("(");
-                memberBuilder.append(parameterTypes.stream()
-                        .map(JavaType::getFullyQualifiedName)
-                        .collect(Collectors.joining(",")));
-                memberBuilder.append(")");
-                resolvedMember = Optional.of(memberBuilder.toString());
-            } else {
-                memberType = Optional.of(MemberType.FIELD);
+    private Optional<ResolvedConstructorDeclaration> findConstructor(
+            ResolvedReferenceTypeDeclaration type, Optional<List<String>> parameterTypes) {
+        return type.getConstructors().stream()
+                .filter(candidate -> matches(candidate, parameterTypes))
+                .findFirst();
+    }
+
+    private boolean matches(ResolvedMethodLikeDeclaration declaration, Optional<List<String>> parameterTypes) {
+        if (!parameterTypes.isPresent()) {
+            return true;
+        }
+        if (declaration.getNumberOfParams() != parameterTypes.get().size()) {
+            return false;
+        }
+        for (int index = 0; index < declaration.getNumberOfParams(); index++) {
+            String actual = declaration.getParam(index).getType().erasure().describe();
+            if (!actual.equals(parameterTypes.get().get(index))) {
+                return false;
             }
-        } else {
-            memberType = Optional.empty();
         }
-        String className = javaClass
-                .getCanonicalName()
-                .substring(javaClass.getPackageName().length() + 1);
-        return Optional.of(new FullyQualifiedJavadocReference(
-                javaClass.getPackageName(),
-                Optional.of(className),
-                resolvedMember,
-                memberType,
-                label,
-                isExternal(javaClass)));
-    }
-
-    private static boolean isClassFound(JavaClass javaClass) {
-        // this is never null due to using the ClassNameLibrary in the builder
-        // but every instance of ClassNameLibrary basically means that the class was not found
-        return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary);
-    }
-
-    // https://github.com/paul-hammant/qdox/issues/104
-    private List<JavaType> getParameterTypes(String member) {
-        List<JavaType> parameterTypes = new ArrayList<>();
-        // TypeResolver.byClassName() always resolves types as non existing inner class
-        TypeResolver typeResolver = TypeResolver.byClassName(
-                declaringClass.getPackageName(),
-                declaringClass.getJavaClassLibrary(),
-                declaringClass.getSource().getImports());
-
-        // method parameters are optionally enclosed by parentheses
-        int indexOfOpeningParenthesis = member.indexOf('(');
-        int indexOfClosingParenthesis = member.indexOf(')');
-        final String signatureArguments;
-        if (indexOfOpeningParenthesis >= 0
-                && indexOfClosingParenthesis > 0
-                && indexOfClosingParenthesis > indexOfOpeningParenthesis) {
-            signatureArguments = member.substring(indexOfOpeningParenthesis + 1, indexOfClosingParenthesis);
-        } else if (indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0
-                || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1) {
+        return true;
+    }
+
+    private Optional<List<String>> parameterTypes(String member) {
+        int opening = member.indexOf('(');
+        int closing = member.lastIndexOf(')');
+        if (opening < 0 && closing < 0) {
+            // Without parentheses, match the first overload found, mirroring javadoc.
+            return Optional.empty();
+        }
+        if (opening < 0 || closing < opening) {
             throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
-        } else {
-            // If any method or constructor is entered as a name with no parentheses, such as getValue,
-            // and if there is no field with the same name, then the javadoc command still creates a
-            // link to the method. If this method is overloaded, then the javadoc command links to the
-            // first method its search encounters, which is unspecified
-            // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654)
-            return Collections.emptyList();
         }
-        for (String parameter : signatureArguments.split(",")) {
-            // strip off argument name, only type is relevant
-            String canonicalParameter = parameter.trim();
-            int spaceIndex = canonicalParameter.indexOf(' ');
-            final String typeName;
-            if (spaceIndex > 0) {
-                typeName = canonicalParameter.substring(0, spaceIndex).trim();
-            } else {
-                typeName = canonicalParameter;
-            }
-            if (!typeName.isEmpty()) {
-                String rawTypeName = getRawTypeName(typeName);
-                // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111
-                if (typeResolver.resolveType(rawTypeName) == null) {
-                    throw new IllegalArgumentException("Found unresolvable method argument type in " + member);
-                }
-                TypeDef typeDef = new TypeDef(getRawTypeName(typeName));
-                int dimensions = getDimensions(typeName);
-                JavaType javaType = TypeAssembler.createUnresolved(typeDef, dimensions, typeResolver);
+        String arguments = member.substring(opening + 1, closing).trim();
+        if (arguments.isEmpty()) {
+            return Optional.of(Collections.emptyList());
+        }
+        List<String> result = new ArrayList<>();
+        for (String argument : splitArguments(arguments)) {
+            result.add(resolveParameterType(stripArgumentName(argument.trim())));
+        }
+        return Optional.of(result);
+    }
 
-                parameterTypes.add(javaType);
+    private static List<String> splitArguments(String arguments) {
+        List<String> result = new ArrayList<>();
+        int genericDepth = 0;
+        int start = 0;
+        for (int index = 0; index < arguments.length(); index++) {
+            char ch = arguments.charAt(index);
+            if (ch == '<') {
+                genericDepth++;
+            } else if (ch == '>') {
+                genericDepth--;
+            } else if (ch == ',' && genericDepth == 0) {
+                result.add(arguments.substring(start, index));
+                start = index + 1;
             }
         }
-        return parameterTypes;
+        result.add(arguments.substring(start));
+        return result;
     }
 
-    private static int getDimensions(String type) {
-        return (int) type.chars().filter(ch -> ch == '[').count();
+    private static String stripArgumentName(String argument) {
+        int genericDepth = 0;
+        for (int index = argument.length() - 1; index >= 0; index--) {
+            char ch = argument.charAt(index);
+            if (ch == '>') {
+                genericDepth++;
+            } else if (ch == '<') {
+                genericDepth--;
+            } else if (Character.isWhitespace(ch) && genericDepth == 0) {
+                return argument.substring(0, index).trim();
+            }
+        }
+        return argument;
     }
 
-    private static String getRawTypeName(String typeName) {
-        // strip dimensions
-        int indexOfOpeningBracket = typeName.indexOf('[');
-        if (indexOfOpeningBracket >= 0) {
-            return typeName.substring(0, indexOfOpeningBracket);
+    private String resolveParameterType(String typeName) {
+        String normalized = eraseGenerics(typeName.replace("...", "[]"));
+        int dimensions = 0;
+        while (normalized.endsWith("[]")) {
+            dimensions++;
+            normalized = normalized.substring(0, normalized.length() - 2);
+        }
+        String resolved;
+        if (isPrimitive(normalized)) {
+            resolved = normalized;
         } else {
-            return typeName;
+            List<String> candidates = new ArrayList<>();
+            candidates.add(normalized);
+            candidates.addAll(classNameCandidates(normalized));
+            resolved = candidates.stream()
+                    .filter(candidate -> sourceModel.resolveType(candidate).isPresent())
+                    .findFirst()
+                    .orElseThrow(
+                            () -> new IllegalArgumentException("Found unresolvable method argument type " + typeName));
         }
+        StringBuilder result = new StringBuilder(resolved);
+        for (int index = 0; index < dimensions; index++) {
+            result.append("[]");
+        }
+        return result.toString();
     }
 
-    private static String getMethodName(String member) {
-        // name is separated from arguments either by '(' or spans the full member
-        int indexOfOpeningParentheses = member.indexOf('(');
-        if (indexOfOpeningParentheses == -1) {
-            return member;
-        } else {
-            return member.substring(0, indexOfOpeningParentheses);
+    private static String eraseGenerics(String value) {
+        StringBuilder result = new StringBuilder();
+        int depth = 0;
+        for (int index = 0; index < value.length(); index++) {
+            char ch = value.charAt(index);
+            if (ch == '<') {
+                depth++;
+            } else if (ch == '>') {
+                depth--;
+            } else if (depth == 0) {
+                result.append(ch);
+            }
+        }
+        return result.toString();
+    }
+
+    private static boolean isPrimitive(String name) {
+        return "boolean".equals(name)
+                || "byte".equals(name)
+                || "char".equals(name)
+                || "double".equals(name)
+                || "float".equals(name)
+                || "int".equals(name)
+                || "long".equals(name)
+                || "short".equals(name);
+    }
+
+    private FullyQualifiedJavadocReference toReference(
+            ResolvedReferenceTypeDeclaration type,
+            Optional<String> member,
+            Optional<MemberType> memberType,
+            Optional<String> label) {
+        return new FullyQualifiedJavadocReference(
+                type.getPackageName(),
+                Optional.of(type.getClassName()),
+                member,
+                memberType,
+                label,
+                !sourceModel.isInternal(type));
+    }
+
+    @Override
+    public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
+        String fqcn = reference
+                .getFullyQualifiedClassName()
+                .orElseThrow(() ->
+                        new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
+        String fieldName = reference
+                .getMember()
+                .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
+        TypeDeclaration<?> type = sourceModel
+                .getType(fqcn)
+                .orElseThrow(() -> new IllegalArgumentException("Could not find source class " + fqcn));
+        for (FieldDeclaration field : type.getFields()) {
+            Optional<com.github.javaparser.ast.body.VariableDeclarator> variable = field.getVariables().stream()
+                    .filter(candidate -> candidate.getNameAsString().equals(fieldName))
+                    .findFirst();
+            if (variable.isPresent()) {
+                if (!field.isStatic()) {
+                    throw new IllegalArgumentException(
+                            "Field with name " + fieldName + " in class " + fqcn + " is not static");
+                }
+                return variable.get()
+                        .getInitializer()
+                        .map(initializer -> initializer.toString())
+                        .orElseThrow(() -> new IllegalArgumentException(
+                                "Field with name " + fieldName + " in class " + fqcn + " has no initializer"));
+            }
         }
+        throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
+    }
+
+    @Override
+    public URI getInternalJavadocSiteBaseUrl() {
+        return linkGenerator.getInternalJavadocSiteBaseUrl();
+    }
+
+    private static ResolvedReferenceTypeDeclaration resolve(TypeDeclaration<?> declaration) {
+        return declaration.resolve();
+    }
+
+    private static String methodName(String member) {
+        int opening = member.indexOf('(');
+        return opening < 0 ? member : member.substring(0, opening);
+    }
+
+    private static String canonicalMember(ResolvedMethodLikeDeclaration declaration) {
+        return declaration.getName() + "("
+                + declaration.formalParameterTypes().stream()
+                        .map(type -> type.erasure().describe())
+                        .collect(Collectors.joining(","))
+                + ")";
+    }
+
+    private static String simpleName(String name) {
+        int separator = name.lastIndexOf('.');
+        return separator < 0 ? name : name.substring(separator + 1);
     }
 
     @SuppressWarnings("unchecked")
diff --git a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
index 579fff05..6b2776d2 100644
--- a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
+++ b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
@@ -20,11 +20,11 @@ package org.apache.maven.tools.plugin.extractor.annotations.converter;
 
 import java.io.File;
 import java.net.URI;
-import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.model.JavaClass;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
 import org.apache.maven.tools.plugin.extractor.annotations.converter.test.CurrentClass;
 import org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
@@ -46,23 +46,24 @@ class JavaClassConverterContextTest {
 
     private final String currentPackageName;
 
-    private final JavaProjectBuilder builder;
+    private final JavaSourceModel sourceModel;
 
-    private final JavaClass contextClass;
+    private final TypeDeclaration<?> contextClass;
 
     private JavadocLinkGenerator linkGenerator;
 
     private URI javadocBaseUri;
 
-    public JavaClassConverterContextTest() throws URISyntaxException {
-        builder = new JavaProjectBuilder();
-        builder.addSourceFolder(new File("src/test/java"));
+    JavaClassConverterContextTest() throws Exception {
+        sourceModel = new JavaSourceModel(StandardCharsets.UTF_8);
+        sourceModel.addSourceDirectory(new File("src/test/java"));
+        sourceModel.parse();
 
-        contextClass = builder.getClassByName(CurrentClass.class.getName());
-        currentPackageName = contextClass.getPackageName();
+        contextClass = sourceModel.getType(CurrentClass.class.getName()).orElseThrow(AssertionError::new);
+        currentPackageName = contextClass.resolve().getPackageName();
         javadocBaseUri = new URI("http://localhost/apidocs");
         linkGenerator = new JavadocLinkGenerator(javadocBaseUri, "11");
-        context = new JavaClassConverterContext(contextClass, builder, Collections.emptyMap(), linkGenerator, 10);
+        context = new JavaClassConverterContext(contextClass, sourceModel, Collections.emptyMap(), linkGenerator, 10);
     }
 
     @Test
@@ -202,7 +203,7 @@ class JavaClassConverterContextTest {
         MojoAnnotatedClass mojoAnnotatedClass = new MojoAnnotatedClass().setMojo(mojoAnnotationContent);
         context = new JavaClassConverterContext(
                 contextClass,
-                builder,
+                sourceModel,
                 Collections.singletonMap(
                         "org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass",
                         mojoAnnotatedClass),