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

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new f65c867e4c GROOVY-11988: Add support for {@inheritDoc} in external JDK 
classes
f65c867e4c is described below

commit f65c867e4c60e43236c0931d494848727c53e814
Author: Daniel Sun <[email protected]>
AuthorDate: Mon May 4 23:13:33 2026 +0900

    GROOVY-11988: Add support for {@inheritDoc} in external JDK classes
---
 .../tools/groovydoc/ExternalGroovyClassDoc.java    |  64 ++-
 .../tools/groovydoc/ExternalJavadocSupport.java    | 617 +++++++++++++++++++++
 .../groovy/tools/groovydoc/GroovyDocTool.java      |  18 +-
 .../tools/groovydoc/SimpleGroovyClassDoc.java      |  40 +-
 .../groovy/tools/groovydoc/TagRenderer.java        | 123 +++-
 .../groovy/tools/groovydoc/GroovyDocToolTest.java  | 153 +++++
 .../testfiles/JavaExtendsWriterInheritDoc.java     |  39 ++
 .../testfiles/JavaImplementsMapInheritDoc.java     | 112 ++++
 .../testfiles/JavaNestedResolutionOuter.java       |  48 ++
 .../JavaNestedResolutionSamePackageConsumer.java   |  30 +
 .../testfiles/JavaObjectCloneInheritDocChild.java  |  27 +
 .../sub/JavaNestedResolutionImportedConsumer.java  |  32 ++
 12 files changed, 1253 insertions(+), 50 deletions(-)

diff --git 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalGroovyClassDoc.java
 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalGroovyClassDoc.java
index cbdbd3ef1b..577670cd35 100644
--- 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalGroovyClassDoc.java
+++ 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalGroovyClassDoc.java
@@ -27,6 +27,7 @@ import org.codehaus.groovy.groovydoc.GroovyMethodDoc;
 import org.codehaus.groovy.groovydoc.GroovyPackageDoc;
 import org.codehaus.groovy.groovydoc.GroovyType;
 
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -41,7 +42,7 @@ public class ExternalGroovyClassDoc implements GroovyClassDoc 
{
     private static final GroovyPackageDoc[] EMPTY_GROOVYPACKAGEDOC_ARRAY = new 
GroovyPackageDoc[0];
     private static final GroovyMethodDoc[] EMPTY_GROOVYMETHODDOC_ARRAY = new 
GroovyMethodDoc[0];
     private static final GroovyType[] EMPTY_GROOVYTYPE_ARRAY = new 
GroovyType[0];
-    private final Class externalClass;
+    private final Class<?> externalClass;
     private final List<GroovyAnnotationRef> annotationRefs;
 
     /**
@@ -49,7 +50,7 @@ public class ExternalGroovyClassDoc implements GroovyClassDoc 
{
      *
      * @param externalClass the reflected class to represent
      */
-    public ExternalGroovyClassDoc(Class externalClass) {
+    public ExternalGroovyClassDoc(Class<?> externalClass) {
         this.externalClass = externalClass;
         annotationRefs = new ArrayList<GroovyAnnotationRef>();
     }
@@ -75,7 +76,8 @@ public class ExternalGroovyClassDoc implements GroovyClassDoc 
{
      */
     @Override
     public String qualifiedTypeName() {
-        return externalClass.getName();
+        String canonicalName = externalClass.getCanonicalName();
+        return canonicalName != null ? canonicalName : externalClass.getName();
     }
 
     /**
@@ -83,15 +85,14 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public GroovyClassDoc superclass() {
-        Class aClass = externalClass.getSuperclass();
-        if (aClass != null) return new ExternalGroovyClassDoc(aClass);
-        return new ExternalGroovyClassDoc(Object.class);
+        Class<?> aClass = externalClass.getSuperclass();
+        return aClass != null ? new ExternalGroovyClassDoc(aClass) : null;
     }
 
     /**
      * Returns the underlying reflected class.
      */
-    public Class externalClass() {
+    public Class<?> externalClass() {
         return externalClass;
     }
 
@@ -107,7 +108,11 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public String simpleTypeName() {
-        return qualifiedTypeName(); // TODO fix
+        String simpleName = externalClass.getSimpleName();
+        if (!simpleName.isEmpty()) return simpleName;
+        String qualifiedName = qualifiedTypeName();
+        int lastDot = qualifiedName.lastIndexOf('.');
+        return lastDot >= 0 ? qualifiedName.substring(lastDot + 1) : 
qualifiedName;
     }
 
     /**
@@ -248,7 +253,14 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public GroovyClassDoc[] interfaces() {
-        return EMPTY_GROOVYCLASSDOC_ARRAY;
+        Class<?>[] interfaces = externalClass.getInterfaces();
+        if (interfaces.length == 0) return EMPTY_GROOVYCLASSDOC_ARRAY;
+
+        GroovyClassDoc[] result = new GroovyClassDoc[interfaces.length];
+        for (int i = 0; i < interfaces.length; i++) {
+            result[i] = new ExternalGroovyClassDoc(interfaces[i]);
+        }
+        return result;
     }
 
     /**
@@ -264,7 +276,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isAbstract() {
-        return false;
+        return Modifier.isAbstract(externalClass.getModifiers());
     }
 
     /**
@@ -280,7 +292,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isSerializable() {
-        return false;
+        return java.io.Serializable.class.isAssignableFrom(externalClass);
     }
 
     /**
@@ -288,7 +300,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public GroovyMethodDoc[] methods() {
-        return EMPTY_GROOVYMETHODDOC_ARRAY;
+        return ExternalJavadocSupport.methodsFor(this);
     }
 
     /**
@@ -296,7 +308,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public GroovyMethodDoc[] methods(boolean filter) {
-        return EMPTY_GROOVYMETHODDOC_ARRAY;
+        return methods();
     }
 
     /**
@@ -360,7 +372,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isFinal() {
-        return false;
+        return Modifier.isFinal(externalClass.getModifiers());
     }
 
     /**
@@ -376,7 +388,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isPrivate() {
-        return false;
+        return Modifier.isPrivate(externalClass.getModifiers());
     }
 
     /**
@@ -384,7 +396,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isProtected() {
-        return false;
+        return Modifier.isProtected(externalClass.getModifiers());
     }
 
     /**
@@ -392,7 +404,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isPublic() {
-        return false;
+        return Modifier.isPublic(externalClass.getModifiers());
     }
 
     /**
@@ -400,7 +412,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isStatic() {
-        return false;
+        return Modifier.isStatic(externalClass.getModifiers());
     }
 
     /**
@@ -416,7 +428,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public int modifierSpecifier() {
-        return 0;
+        return externalClass.getModifiers();
     }
 
     /**
@@ -424,7 +436,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public String qualifiedName() {
-        return null;
+        return externalClass.getName();
     }
 
     /**
@@ -448,7 +460,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isAnnotationType() {
-        return false;
+        return externalClass.isAnnotation();
     }
 
     /**
@@ -464,7 +476,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isClass() {
-        return false;
+        return !externalClass.isInterface() && !externalClass.isAnnotation() 
&& !externalClass.isEnum() && !externalClass.isRecord();
     }
 
     /**
@@ -488,7 +500,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isEnum() {
-        return false;
+        return externalClass.isEnum();
     }
 
     /**
@@ -496,7 +508,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isRecord() {
-        return false;
+        return externalClass.isRecord();
     }
 
     /**
@@ -544,7 +556,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isInterface() {
-        return false;
+        return externalClass.isInterface();
     }
 
     /**
@@ -560,7 +572,7 @@ public class ExternalGroovyClassDoc implements 
GroovyClassDoc {
      */
     @Override
     public boolean isOrdinaryClass() {
-        return false;
+        return isClass();
     }
 
     /**
diff --git 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalJavadocSupport.java
 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalJavadocSupport.java
new file mode 100644
index 0000000000..bd6f13716e
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/ExternalJavadocSupport.java
@@ -0,0 +1,617 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc;
+
+import com.github.javaparser.JavaParser;
+import com.github.javaparser.ParseResult;
+import com.github.javaparser.ParserConfiguration;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.body.BodyDeclaration;
+import com.github.javaparser.ast.body.MethodDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.nodeTypes.NodeWithTypeParameters;
+import org.codehaus.groovy.groovydoc.GroovyMethodDoc;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Loads Javadoc for external classes (primarily JDK classes) from the local
+ * JDK source archive so {@code {@inheritDoc}} can be expanded when a Groovy
+ * source method overrides a method declared outside the documented source set.
+ */
+final class ExternalJavadocSupport {
+    private static final JavaParser JAVA_PARSER = new JavaParser(
+            new 
ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE)
+    );
+    private static final Path JDK_SRC_ZIP = detectJdkSrcZip();
+    private static final Map<Class<?>, Map<MethodKey, String>> 
RAW_COMMENT_CACHE = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, List<ExternalMethodData>> METHOD_CACHE 
= new ConcurrentHashMap<>();
+    private static final Map<Class<?>, GroovyMethodDoc[]> METHOD_DOC_CACHE = 
new ConcurrentHashMap<>();
+    private static final AtomicInteger ACTIVE_CACHE_SESSIONS = new 
AtomicInteger();
+    private static final GroovyMethodDoc[] EMPTY_GROOVYMETHODDOC_ARRAY = new 
GroovyMethodDoc[0];
+
+    private ExternalJavadocSupport() {
+    }
+
+    /**
+     * Returns the groovydoc representation of methods declared in the 
external class,
+     * with comments resolved from the JDK source archive. External method 
comments
+     * containing {@code {@inheritDoc}} are recursively resolved to their 
parent
+     * class or interface method documentation.
+     *
+     * @param owner the external class documentation wrapper
+     * @return array of groovydoc method representations; empty if no methods 
are found
+     */
+    static GroovyMethodDoc[] methodsFor(ExternalGroovyClassDoc owner) {
+        if (ACTIVE_CACHE_SESSIONS.get() == 0) {
+            try (CacheSession ignored = openCacheSession()) {
+                return cachedMethodDocsFor(owner.externalClass());
+            }
+        }
+        return cachedMethodDocsFor(owner.externalClass());
+    }
+
+    /**
+     * Opens a new cache session for external Javadoc loading. While a session 
is
+     * active, external class method documentation and comment metadata are 
cached
+     * and reused across multiple lookups. When the last session closes, all 
caches
+     * are automatically cleared to avoid long-term memory retention in 
long-lived
+     * Gradle daemons.
+     *
+     * <p>This method should be called at the start of a batch groovydoc 
rendering
+     * operation that will perform multiple external inheritDoc lookups.</p>
+     *
+     * @return a {@link CacheSession} that must be closed (typically via 
try-with-resources)
+     */
+    static CacheSession openCacheSession() {
+        ACTIVE_CACHE_SESSIONS.incrementAndGet();
+        return new CacheSession();
+    }
+
+    /**
+     * Returns current statistics about the state of all external Javadoc 
caches,
+     * including the number of cached raw comment texts, method metadata 
entries,
+     * and fully-materialized method doc arrays.
+     *
+     * @return a {@link CacheStats} snapshot capturing all three cache sizes
+     */
+    static CacheStats cacheStats() {
+        return new CacheStats(RAW_COMMENT_CACHE.size(), METHOD_CACHE.size(), 
METHOD_DOC_CACHE.size());
+    }
+
+    /**
+     * Clears all external Javadoc caches. This method is automatically called 
when
+     * the last active {@link CacheSession} is closed. It can also be called 
manually
+     * to force a reset of cached data.
+     */
+    static void clearCaches() {
+        RAW_COMMENT_CACHE.clear();
+        METHOD_CACHE.clear();
+        METHOD_DOC_CACHE.clear();
+    }
+
+    private static GroovyMethodDoc[] cachedMethodDocsFor(Class<?> 
externalClass) {
+        return METHOD_DOC_CACHE.computeIfAbsent(externalClass, 
ExternalJavadocSupport::loadMethodDocs);
+    }
+
+    private static List<ExternalMethodData> loadExternalMethods(Class<?> 
externalClass) {
+        Method[] declaredMethods = externalClass.getDeclaredMethods();
+        Arrays.sort(declaredMethods, Comparator.comparing(Method::getName)
+                .thenComparingInt(Method::getParameterCount)
+                .thenComparing(Method::toGenericString));
+
+        List<ExternalMethodData> result = new ArrayList<>();
+        for (Method method : declaredMethods) {
+            if (method.isSynthetic() || method.isBridge()) continue;
+            result.add(new ExternalMethodData(
+                    method.getName(),
+                    typeName(method.getReturnType()),
+                    parameterTypeNames(method),
+                    resolveEffectiveComment(externalClass, method, new 
HashSet<>())
+            ));
+        }
+        return result;
+    }
+
+    private static GroovyMethodDoc[] loadMethodDocs(Class<?> externalClass) {
+        List<ExternalMethodData> methods = 
METHOD_CACHE.computeIfAbsent(externalClass, 
ExternalJavadocSupport::loadExternalMethods);
+        if (methods.isEmpty()) return EMPTY_GROOVYMETHODDOC_ARRAY;
+
+        ExternalGroovyClassDoc owner = new 
ExternalGroovyClassDoc(externalClass);
+        GroovyMethodDoc[] docs = new GroovyMethodDoc[methods.size()];
+        for (int i = 0; i < methods.size(); i++) {
+            docs[i] = methods.get(i).toMethodDoc(owner);
+        }
+        return docs;
+    }
+
+    private static Map<MethodKey, String> loadMethodComments(Class<?> 
externalClass) {
+        return RAW_COMMENT_CACHE.computeIfAbsent(externalClass, 
ExternalJavadocSupport::parseMethodComments);
+    }
+
+    /**
+     * Manages the lifecycle of external Javadoc caches for a single groovydoc 
render session.
+     * Implements reference counting: when the last session closes, all 
external caches are
+     * cleared to prevent long-term memory retention in the Gradle daemon.
+     *
+     * <p>This class is not intended for public use; obtain instances via
+     * {@link ExternalJavadocSupport#openCacheSession()}.</p>
+     */
+    static final class CacheSession implements AutoCloseable {
+        private boolean closed;
+
+        @Override
+        public void close() {
+            if (closed) return;
+            closed = true;
+
+            int remaining = ACTIVE_CACHE_SESSIONS.decrementAndGet();
+            if (remaining <= 0) {
+                ACTIVE_CACHE_SESSIONS.set(0);
+                clearCaches();
+            }
+        }
+    }
+
+    /**
+     * Snapshot of current external Javadoc cache statistics. Contains the size
+     * of each of the three caches: raw comment text, method metadata, and 
fully
+     * materialized method documentation arrays.
+     *
+     * <p>This class is immutable and used for diagnostics and testing.</p>
+     */
+    static final class CacheStats {
+        private final int rawCommentCacheSize;
+        private final int methodCacheSize;
+        private final int methodDocCacheSize;
+
+        private CacheStats(int rawCommentCacheSize, int methodCacheSize, int 
methodDocCacheSize) {
+            this.rawCommentCacheSize = rawCommentCacheSize;
+            this.methodCacheSize = methodCacheSize;
+            this.methodDocCacheSize = methodDocCacheSize;
+        }
+
+        /**
+         * Returns the number of external classes with cached raw Javadoc 
comment text.
+         *
+         * @return the size of the raw comment cache
+         */
+        int rawCommentCacheSize() {
+            return rawCommentCacheSize;
+        }
+
+        /**
+         * Returns the number of external classes with cached method metadata 
(method names,
+         * parameter types, return types).
+         *
+         * @return the size of the method metadata cache
+         */
+        int methodCacheSize() {
+            return methodCacheSize;
+        }
+
+        /**
+         * Returns the number of external classes with cached 
fully-materialized method
+         * documentation arrays ({@code GroovyMethodDoc[]}).
+         *
+         * @return the size of the method documentation cache
+         */
+        int methodDocCacheSize() {
+            return methodDocCacheSize;
+        }
+    }
+
+    private static Map<MethodKey, String> parseMethodComments(Class<?> 
externalClass) {
+        Map<MethodKey, String> comments = new LinkedHashMap<>();
+        Optional<CompilationUnit> source = loadCompilationUnit(externalClass);
+        if (source.isEmpty()) return comments;
+
+        Optional<TypeDeclaration<?>> type = findTypeDeclaration(source.get(), 
externalClass);
+        if (type.isEmpty()) return comments;
+
+        for (BodyDeclaration<?> member : type.get().getMembers()) {
+            if (!(member instanceof MethodDeclaration methodDeclaration)) 
continue;
+            Method reflectionMethod = 
findMatchingDeclaredMethod(externalClass, methodDeclaration);
+            if (reflectionMethod == null) continue;
+            String raw = methodDeclaration.getJavadocComment()
+                    .map(comment -> 
normalizeJavadocComment(comment.getContent()))
+                    .orElse("");
+            comments.put(MethodKey.of(reflectionMethod), raw);
+        }
+        return comments;
+    }
+
+    private static String resolveEffectiveComment(Class<?> ownerClass, Method 
method, Set<ExternalMethodKey> visited) {
+        ExternalMethodKey key = new ExternalMethodKey(ownerClass, 
MethodKey.of(method));
+        if (!visited.add(key)) return "";
+
+        String rawComment = 
loadMethodComments(ownerClass).getOrDefault(key.methodKey(), "");
+        String trimmed = rawComment.trim();
+        if (!trimmed.contains("{@inheritDoc}")) return rawComment;
+
+        ExternalMethodMatch inherited = findInheritedMethod(ownerClass, 
method, new HashSet<>());
+        if (inherited == null) {
+            return rawComment.replace("{@inheritDoc}", "").trim();
+        }
+
+        String inheritedComment = 
resolveEffectiveComment(inherited.ownerClass(), inherited.method(), visited);
+        if (trimmed.equals("{@inheritDoc}")) {
+            return inheritedComment;
+        }
+        return rawComment.replace("{@inheritDoc}", inheritedComment);
+    }
+
+    private static Optional<CompilationUnit> loadCompilationUnit(Class<?> 
externalClass) {
+        if (JDK_SRC_ZIP == null) return Optional.empty();
+        String entryName = sourceEntryName(externalClass);
+        if (entryName == null) return Optional.empty();
+
+        try (ZipFile zip = new ZipFile(JDK_SRC_ZIP.toFile())) {
+            ZipEntry entry = zip.getEntry(entryName);
+            if (entry == null) {
+                entry = findFallbackEntry(zip, entryName);
+                if (entry == null) return Optional.empty();
+            }
+            try (InputStream inputStream = zip.getInputStream(entry)) {
+                String source = new String(inputStream.readAllBytes(), 
StandardCharsets.UTF_8);
+                ParseResult<CompilationUnit> result = 
JAVA_PARSER.parse(source);
+                return result.getResult();
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static ZipEntry findFallbackEntry(ZipFile zip, String entryName) {
+        int slash = entryName.indexOf('/');
+        String suffix = slash >= 0 ? "/" + entryName.substring(slash + 1) : 
"/" + entryName;
+        return zip.stream()
+                .filter(candidate -> candidate.getName().endsWith(suffix))
+                .findFirst()
+                .orElse(null);
+    }
+
+    private static Optional<TypeDeclaration<?>> 
findTypeDeclaration(CompilationUnit compilationUnit, Class<?> externalClass) {
+        String packageName = externalClass.getPackageName();
+        String binaryName = externalClass.getName();
+        String relativeName = packageName.isEmpty() ? binaryName : 
binaryName.substring(packageName.length() + 1);
+        String[] segments = relativeName.split("\\$");
+
+        TypeDeclaration<?> current = null;
+        for (TypeDeclaration<?> typeDeclaration : compilationUnit.getTypes()) {
+            if (typeDeclaration.getNameAsString().equals(segments[0])) {
+                current = typeDeclaration;
+                break;
+            }
+        }
+        if (current == null) return Optional.empty();
+
+        for (int i = 1; i < segments.length; i++) {
+            String segment = segments[i];
+            if (segment.chars().allMatch(Character::isDigit)) return 
Optional.empty();
+            TypeDeclaration<?> next = null;
+            for (BodyDeclaration<?> member : current.getMembers()) {
+                if (member instanceof TypeDeclaration<?> nested && 
nested.getNameAsString().equals(segment)) {
+                    next = nested;
+                    break;
+                }
+            }
+            if (next == null) return Optional.empty();
+            current = next;
+        }
+        return Optional.of(current);
+    }
+
+    private static Method findMatchingDeclaredMethod(Class<?> externalClass, 
MethodDeclaration methodDeclaration) {
+        Method[] declaredMethods = externalClass.getDeclaredMethods();
+        for (Method method : declaredMethods) {
+            if (!method.getName().equals(methodDeclaration.getNameAsString())) 
continue;
+            if (method.isSynthetic() || method.isBridge()) continue;
+            if (method.getParameterCount() != 
methodDeclaration.getParameters().size()) continue;
+
+            boolean allMatch = true;
+            for (int i = 0; i < method.getParameterCount(); i++) {
+                String declaredType = 
methodDeclaration.getParameter(i).getType().asString();
+                if (methodDeclaration.getParameter(i).isVarArgs()) {
+                    declaredType += "[]";
+                }
+                if (!matchesTypeName(declaredType, 
method.getParameterTypes()[i], methodDeclaration, externalClass)) {
+                    allMatch = false;
+                    break;
+                }
+            }
+            if (allMatch) return method;
+        }
+        return null;
+    }
+
+    private static boolean matchesTypeName(String declaredType, Class<?> 
reflectedType, MethodDeclaration methodDeclaration, Class<?> externalClass) {
+        String normalizedDeclared = eraseTypeName(declaredType.replace("...", 
"[]").trim());
+        if (normalizedDeclared.equals(typeName(reflectedType))) return true;
+        if (normalizedDeclared.equals(reflectedType.getSimpleName())) return 
true;
+        if (normalizedDeclared.equals(reflectedType.getTypeName())) return 
true;
+        if (reflectedType.getCanonicalName() != null && 
normalizedDeclared.equals(reflectedType.getCanonicalName())) return true;
+
+        Set<String> typeParameters = new HashSet<>();
+        methodDeclaration.getTypeParameters().forEach(type -> 
typeParameters.add(type.getNameAsString()));
+        methodDeclaration.findAncestor(TypeDeclaration.class)
+                .ifPresent(type -> {
+                    if (type instanceof NodeWithTypeParameters<?> 
nodeWithTypeParameters) {
+                        nodeWithTypeParameters.getTypeParameters()
+                                .forEach(parameter -> 
typeParameters.add(parameter.getNameAsString()));
+                    }
+                });
+        if (typeParameters.contains(normalizedDeclared)) {
+            return reflectedType == Object.class;
+        }
+        if (normalizedDeclared.endsWith("[]")) {
+            String componentType = normalizedDeclared.substring(0, 
normalizedDeclared.length() - 2);
+            if (typeParameters.contains(componentType)) {
+                return reflectedType.isArray() && 
reflectedType.getComponentType() == Object.class;
+            }
+        }
+
+        if (!normalizedDeclared.contains(".")) {
+            String packagePrefix = externalClass.getPackageName();
+            if (!packagePrefix.isEmpty() && (packagePrefix + "." + 
normalizedDeclared).equals(typeName(reflectedType))) return true;
+            if (("java.lang." + 
normalizedDeclared).equals(typeName(reflectedType))) return true;
+        }
+
+        return false;
+    }
+
+    private static String eraseTypeName(String declaredType) {
+        if (declaredType == null || declaredType.isEmpty()) return "";
+        StringBuilder erased = new StringBuilder(declaredType.length());
+        int genericDepth = 0;
+        for (int i = 0; i < declaredType.length(); i++) {
+            char ch = declaredType.charAt(i);
+            if (ch == '<') {
+                genericDepth++;
+                continue;
+            }
+            if (ch == '>') {
+                genericDepth--;
+                continue;
+            }
+            if (genericDepth == 0) {
+                erased.append(ch);
+            }
+        }
+        String normalized = erased.toString().trim();
+        if (normalized.startsWith("? extends ")) {
+            normalized = normalized.substring("? extends ".length()).trim();
+        } else if (normalized.startsWith("? super ")) {
+            normalized = normalized.substring("? super ".length()).trim();
+        } else if ("?".equals(normalized)) {
+            return Object.class.getSimpleName();
+        }
+        return normalized;
+    }
+
+    private static ExternalMethodMatch findInheritedMethod(Class<?> 
ownerClass, Method method, Set<Class<?>> seen) {
+        Class<?> superclass = ownerClass.getSuperclass();
+        while (superclass != null && seen.add(superclass)) {
+            Method declared = findDeclaredMethod(superclass, method);
+            if (declared != null) return new ExternalMethodMatch(superclass, 
declared);
+            superclass = superclass.getSuperclass();
+        }
+
+        ExternalMethodMatch direct = findInheritedInterfaceMethod(ownerClass, 
method, seen);
+        if (direct != null) return direct;
+
+        for (Class<?> current = ownerClass.getSuperclass(); current != null; 
current = current.getSuperclass()) {
+            ExternalMethodMatch inherited = 
findInheritedInterfaceMethod(current, method, seen);
+            if (inherited != null) return inherited;
+        }
+        return null;
+    }
+
+    private static ExternalMethodMatch findInheritedInterfaceMethod(Class<?> 
type, Method method, Set<Class<?>> seen) {
+        for (Class<?> iface : type.getInterfaces()) {
+            if (!seen.add(iface)) continue;
+            Method declared = findDeclaredMethod(iface, method);
+            if (declared != null) return new ExternalMethodMatch(iface, 
declared);
+            ExternalMethodMatch deeper = findInheritedInterfaceMethod(iface, 
method, seen);
+            if (deeper != null) return deeper;
+        }
+        return null;
+    }
+
+    private static Method findDeclaredMethod(Class<?> type, Method template) {
+        try {
+            Method method = type.getDeclaredMethod(template.getName(), 
template.getParameterTypes());
+            return method.isSynthetic() || method.isBridge() ? null : method;
+        } catch (NoSuchMethodException ignored) {
+            return null;
+        }
+    }
+
+    private static String sourceEntryName(Class<?> externalClass) {
+        String binaryName = externalClass.getName();
+        String packageName = externalClass.getPackageName();
+        String relativeName = packageName.isEmpty() ? binaryName : 
binaryName.substring(packageName.length() + 1);
+        int nested = relativeName.indexOf('$');
+        String topLevel = nested >= 0 ? relativeName.substring(0, nested) : 
relativeName;
+        StringBuilder entry = new StringBuilder();
+        Module module = externalClass.getModule();
+        if (module != null && module.isNamed()) {
+            entry.append(module.getName()).append('/');
+        }
+        if (!packageName.isEmpty()) {
+            entry.append(packageName.replace('.', '/')).append('/');
+        }
+        entry.append(topLevel).append(".java");
+        return entry.toString();
+    }
+
+    private static String typeName(Class<?> type) {
+        if (type.isArray()) return typeName(type.getComponentType()) + "[]";
+        String canonicalName = type.getCanonicalName();
+        return canonicalName != null ? canonicalName : type.getTypeName();
+    }
+
+    private static String normalizeJavadocComment(String content) {
+        if (content == null || content.isEmpty()) return "";
+        String[] lines = content.replace("\r\n", "\n").replace('\r', 
'\n').split("\n", -1);
+        int start = 0;
+        int end = lines.length;
+        while (start < end && lines[start].trim().isEmpty()) {
+            start++;
+        }
+        while (end > start && lines[end - 1].trim().isEmpty()) {
+            end--;
+        }
+        StringBuilder normalized = new StringBuilder();
+        for (int i = start; i < end; i++) {
+            if (normalized.length() > 0) normalized.append('\n');
+            normalized.append(lines[i].replaceFirst("^\\s*\\* ?", ""));
+        }
+        return normalized.toString().trim();
+    }
+
+    private static List<String> parameterTypeNames(Method method) {
+        Class<?>[] parameterTypes = method.getParameterTypes();
+        List<String> result = new ArrayList<>(parameterTypes.length);
+        for (Class<?> parameterType : parameterTypes) {
+            result.add(typeName(parameterType));
+        }
+        return result;
+    }
+
+    private static Path detectJdkSrcZip() {
+        String javaHome = System.getProperty("java.home");
+        if (javaHome == null || javaHome.isEmpty()) return null;
+
+        Path home = Path.of(javaHome);
+        Path direct = home.resolve("lib/src.zip");
+        if (Files.isRegularFile(direct)) return direct;
+
+        Path parent = home.getParent();
+        if (parent == null) return null;
+
+        Path sibling = parent.resolve("lib/src.zip");
+        return Files.isRegularFile(sibling) ? sibling : null;
+    }
+
+    /**
+     * Represents method metadata extracted from an external class (typically 
JDK classes).
+     * Captures the method signature (name, parameter types, return type) and 
its raw
+     * Javadoc comment text. Used as an intermediate representation before 
converting
+     * to {@link SimpleGroovyMethodDoc} for rendering.
+     *
+     * <p>The raw comment text may contain {@code {@inheritDoc}} markers that 
are
+     * expanded during cache construction.</p>
+     */
+    private static final class ExternalMethodData {
+        private final String name;
+        private final String returnTypeName;
+        private final List<String> parameterTypeNames;
+        private final String rawCommentText;
+
+        private ExternalMethodData(String name, String returnTypeName, 
List<String> parameterTypeNames, String rawCommentText) {
+            this.name = name;
+            this.returnTypeName = returnTypeName;
+            this.parameterTypeNames = parameterTypeNames;
+            this.rawCommentText = rawCommentText;
+        }
+
+        /**
+         * Converts this method data into a fully-materialized {@link 
SimpleGroovyMethodDoc}
+         * suitable for rendering by groovydoc templates. Sets up method name, 
return type,
+         * parameters, and raw comment text.
+         *
+         * @param owner the groovydoc representation of the external class 
that owns this method
+         * @return a groovydoc method representation with all fields populated
+         */
+        private GroovyMethodDoc toMethodDoc(ExternalGroovyClassDoc owner) {
+            SimpleGroovyMethodDoc methodDoc = new SimpleGroovyMethodDoc(name, 
owner);
+            methodDoc.setReturnType(new SimpleGroovyType(returnTypeName));
+            for (int i = 0; i < parameterTypeNames.size(); i++) {
+                SimpleGroovyParameter parameter = new 
SimpleGroovyParameter("arg" + i);
+                parameter.setType(new 
SimpleGroovyType(parameterTypeNames.get(i)));
+                methodDoc.add(parameter);
+            }
+            methodDoc.setRawCommentText(rawCommentText);
+            return methodDoc;
+        }
+    }
+
+    /**
+     * Uniquely identifies a method within an external class by its name and
+     * parameter type names. Used as a cache key for storing and retrieving
+     * Javadoc comment text for specific methods.
+     *
+     * @param name the method name
+     * @param parameterTypeNames the qualified names of parameter types in 
order
+     */
+    private record MethodKey(String name, List<String> parameterTypeNames) {
+        /**
+         * Creates a {@code MethodKey} from a reflected {@link Method}.
+         *
+         * @param method the reflected method
+         * @return a cache key representing this method
+         */
+        private static MethodKey of(Method method) {
+            return new MethodKey(method.getName(), 
ExternalJavadocSupport.parameterTypeNames(method));
+        }
+    }
+
+    /**
+     * Uniquely identifies a method within a specific external class hierarchy
+     * by combining the owner class with a method key. Used during recursive
+     * resolution of {@code {@inheritDoc}} to prevent infinite loops when
+     * cyclic inheritance patterns are encountered.
+     *
+     * @param ownerClass the class declaring the method
+     * @param methodKey the method identifier (name and parameter types)
+     */
+    private record ExternalMethodKey(Class<?> ownerClass, MethodKey methodKey) 
{
+    }
+
+    /**
+     * Represents a method found while walking an external class's inheritance 
chain
+     * during {@code {@inheritDoc}} resolution. Pairs the class that declares 
the method
+     * with the reflected method object itself.
+     *
+     * @param ownerClass the class in which this method is declared
+     * @param method the reflected method object
+     */
+    private record ExternalMethodMatch(Class<?> ownerClass, Method method) {
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/GroovyDocTool.java
 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/GroovyDocTool.java
index dcb8bd9192..ded95f7253 100644
--- 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/GroovyDocTool.java
+++ 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/GroovyDocTool.java
@@ -172,14 +172,16 @@ public class GroovyDocTool {
         if ("true".equals(properties.getProperty("privateScope"))) 
properties.setProperty("packageScope", "true");
         if ("true".equals(properties.getProperty("packageScope"))) 
properties.setProperty("protectedScope", "true");
         if ("true".equals(properties.getProperty("protectedScope"))) 
properties.setProperty("publicScope", "true");
-        if (templateEngine != null) {
-            GroovyDocWriter writer = new GroovyDocWriter(output, 
templateEngine, properties, sourcepaths);
-            GroovyRootDoc rootDoc = rootDocBuilder.getRootDoc();
-            writer.writeRoot(rootDoc, destdir);
-            writer.writePackages(rootDoc, destdir);
-            writer.writeClasses(rootDoc, destdir);
-        } else {
-            throw new UnsupportedOperationException("No template engine was 
found");
+        try (AutoCloseable ignored = 
ExternalJavadocSupport.openCacheSession()) {
+            if (templateEngine != null) {
+                GroovyDocWriter writer = new GroovyDocWriter(output, 
templateEngine, properties, sourcepaths);
+                GroovyRootDoc rootDoc = rootDocBuilder.getRootDoc();
+                writer.writeRoot(rootDoc, destdir);
+                writer.writePackages(rootDoc, destdir);
+                writer.writeClasses(rootDoc, destdir);
+            } else {
+                throw new UnsupportedOperationException("No template engine 
was found");
+            }
         }
     }
 
diff --git 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/SimpleGroovyClassDoc.java
 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/SimpleGroovyClassDoc.java
index 7a9cfd5ac5..9fdd2a51bc 100644
--- 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/SimpleGroovyClassDoc.java
+++ 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/SimpleGroovyClassDoc.java
@@ -774,6 +774,8 @@ public class SimpleGroovyClassDoc extends 
SimpleGroovyAbstractableElementDoc imp
                 if (doc != null) return doc;
                 doc = resolveInternalClassDocFromSamePackage(rootDoc, name);
                 if (doc != null) return doc;
+                doc = resolveNestedClassDocFromEnclosingTypes(rootDoc, name);
+                if (doc != null) return doc;
                 for (GroovyClassDoc nestedDoc : nested) {
                     if (nestedDoc.name().endsWith("." + name))
                         return nestedDoc;
@@ -851,12 +853,32 @@ public class SimpleGroovyClassDoc extends 
SimpleGroovyAbstractableElementDoc imp
         return PRIMITIVES.contains(type);
     }
 
+    private static String normalizeInternalTypeName(String name) {
+        return name.replace('$', '.');
+    }
+
+    private static int lastInternalNestedSeparator(String fullPathName) {
+        int lastSlash = fullPathName.lastIndexOf('/');
+        int lastDot = fullPathName.lastIndexOf('.');
+        return lastDot > lastSlash ? lastDot : -1;
+    }
+
     private GroovyClassDoc resolveInternalClassDocFromImport(GroovyRootDoc 
rootDoc, String baseName) {
         if (isPrimitiveType(baseName)) return null;
+        String normalizedBaseName = normalizeInternalTypeName(baseName);
         for (String importName : importedClassesAndPackages) {
             String targetClassName = null;
             if (aliases.containsKey(baseName)) {
                 targetClassName = aliases.get(baseName);
+            } else if (normalizedBaseName.contains(".")) {
+                int dot = normalizedBaseName.indexOf('.');
+                String outerName = normalizedBaseName.substring(0, dot);
+                String nestedSuffix = normalizedBaseName.substring(dot);
+                if (importName.endsWith("/" + outerName)) {
+                    targetClassName = importName + nestedSuffix;
+                } else if (importName.endsWith("/*")) {
+                    targetClassName = importName.substring(0, 
importName.length() - 1) + normalizedBaseName;
+                }
             } else if (importName.endsWith("/" + baseName)) {
                 targetClassName = importName;
             } else if (importName.endsWith("/*")) {
@@ -882,11 +904,25 @@ public class SimpleGroovyClassDoc extends 
SimpleGroovyAbstractableElementDoc imp
 
     private GroovyClassDoc 
resolveInternalClassDocFromSamePackage(GroovyRootDoc rootDoc, String baseName) {
         if (isPrimitiveType(baseName)) return null;
-        if (baseName.contains(".")) return null;
         int lastSlash = fullPathName.lastIndexOf('/');
         if (lastSlash < 0) return null;
         String pkg = fullPathName.substring(0, lastSlash + 1);
-        return ((SimpleGroovyRootDoc)rootDoc).classNamedExact(pkg + baseName);
+        String candidate = normalizeInternalTypeName(baseName);
+        return ((SimpleGroovyRootDoc)rootDoc).classNamedExact(pkg + candidate);
+    }
+
+    private GroovyClassDoc 
resolveNestedClassDocFromEnclosingTypes(GroovyRootDoc rootDoc, String baseName) 
{
+        if (rootDoc == null || fullPathName == null) return null;
+        String nestedSuffix = normalizeInternalTypeName(baseName);
+        String current = fullPathName;
+        int separator = lastInternalNestedSeparator(current);
+        while (separator >= 0) {
+            current = current.substring(0, separator);
+            GroovyClassDoc doc = ((SimpleGroovyRootDoc) 
rootDoc).classNamedExact(current + "." + nestedSuffix);
+            if (doc != null) return doc;
+            separator = lastInternalNestedSeparator(current);
+        }
+        return null;
     }
 
     private Class resolveExternalClassFromImport(String name) {
diff --git 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/TagRenderer.java
 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/TagRenderer.java
index 9122c9abcf..7fc8960192 100644
--- 
a/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/TagRenderer.java
+++ 
b/subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/TagRenderer.java
@@ -874,7 +874,7 @@ final class TagRenderer {
 
     private static boolean isKnownInlineTag(String name, Config cfg) {
         return switch (name) {
-            case "link", "see", "code", "interface", "value", "inheritDoc", 
"snippet" -> true;
+            case "link", "linkplain", "see", "code", "interface", "value", 
"inheritDoc", "return", "summary", "index", "snippet" -> true;
             case "literal" -> cfg.literalEnabled;
             default -> false;
         };
@@ -893,6 +893,7 @@ final class TagRenderer {
                 // annotation declarations in comments don't pollute output.
                 return;
             case "link":
+            case "linkplain":
             case "see":
                 out.append(SimpleGroovyClassDoc.getDocUrl(body, false, links, 
relPath, rootDoc, classDoc));
                 return;
@@ -905,6 +906,11 @@ final class TagRenderer {
             case "code":
                 
out.append(cfg.codeOpen).append(SimpleGroovyClassDoc.encodeAngleBrackets(body)).append(cfg.codeClose);
                 return;
+            case "return":
+            case "summary":
+            case "index":
+                out.append(renderInline(body, links, relPath, rootDoc, 
classDoc, memberDoc, cfg, inheritDocVisited, inheritDocContext));
+                return;
             case "value": {
                 // GROOVY-6016: resolve {@value #FIELD} or {@value 
Class#FIELD}.
                 // Bare {@value} resolves to the enclosing field's own value
@@ -924,7 +930,9 @@ final class TagRenderer {
             case "inheritDoc": {
                 // GROOVY-3782: only meaningful on a method; render the parent
                 // method's doc in the same inheritDoc expansion context so
-                // cycles don't reset the visited set on re-entry.
+                // cycles don't reset the visited set on re-entry. A non-null
+                // result means the tag was handled; the empty string 
suppresses
+                // a literal {@inheritDoc} when no inherited text is available.
                 String inherited = resolveInheritDoc(memberDoc, classDoc, 
links, relPath, rootDoc, cfg, inheritDocVisited, inheritDocContext);
                 if (inherited != null) {
                     out.append(inherited);
@@ -944,8 +952,11 @@ final class TagRenderer {
      * already-rendered comment text. Walks the superclass chain, then
      * interfaces reachable from the current class or any superclass,
      * looking for a method with the same name and matching parameter type
-     * names. Returns {@code null} if the current member isn't a method,
-     * no parent method is found, or the parent method has no doc.
+     * names. Returns {@code null} only when the current member isn't a
+     * method and the tag should therefore remain verbatim. Returns an empty
+     * string when the tag is recognized in a method context but no inherited
+     * text should be emitted, for example because no parent doc is available
+     * or a cycle was detected.
      *
      * <p>Recursion safety: the {@code visited} set tracks methods we've
      * already expanded on this chain and is reused when rendering parent
@@ -961,20 +972,22 @@ final class TagRenderer {
                                             Set<GroovyMethodDoc> visited,
                                             InheritDocContext 
inheritDocContext) {
         if (!(memberDoc instanceof GroovyMethodDoc thisMethod)) return null;
-        if (classDoc == null) return null;
+        if (classDoc == null) return "";
         if (visited == null) visited = new HashSet<>();
         if (!visited.add(thisMethod)) return ""; // cycle: suppress literal 
{@inheritDoc}
 
         GroovyMethodDoc parent = findInheritedMethod(thisMethod, classDoc, new 
HashSet<>());
-        if (parent == null) return null;
-        if (parent instanceof SimpleGroovyMemberDoc parentMember
-                && parentMember.belongsToClass instanceof SimpleGroovyClassDoc 
parentClassDoc) {
+        if (parent == null) return "";
+        if (parent instanceof SimpleGroovyMemberDoc parentMember) {
+            SimpleGroovyClassDoc parentClassDoc = parentMember.belongsToClass 
instanceof SimpleGroovyClassDoc
+                    ? (SimpleGroovyClassDoc) parentMember.belongsToClass
+                    : classDoc;
             if (inheritDocContext != null) {
                 GroovyTag inheritedTag = findInheritedTag(parentMember, 
inheritDocContext);
-                if (inheritedTag == null) return null;
+                if (inheritedTag == null) return "";
                 return renderInline(inheritedTag.text(), links, relPath, 
rootDoc, parentClassDoc, parentMember, cfg, visited, inheritDocContext);
             }
-            return 
parentClassDoc.replaceTags(parentMember.getRawCommentText(), parentMember, 
visited);
+            return render(parentMember.getRawCommentText(), links, relPath, 
rootDoc, parentClassDoc, parentMember, cfg, visited);
         }
         String rendered = parent.commentText();
         return rendered == null ? "" : rendered;
@@ -1049,19 +1062,101 @@ final class TagRenderer {
     private static GroovyMethodDoc findMatchingMethod(GroovyClassDoc cls, 
GroovyMethodDoc target) {
         String targetName = target.name();
         GroovyParameter[] targetParams = target.parameters();
+        GroovyMethodDoc compatibleMatch = null;
         for (GroovyMethodDoc m : cls.methods()) {
             if (!targetName.equals(m.name())) continue;
             GroovyParameter[] params = m.parameters();
             if (params.length != targetParams.length) continue;
-            boolean allMatch = true;
+            boolean allExactMatch = true;
+            boolean allCompatible = true;
             for (int i = 0; i < params.length; i++) {
                 String a = params[i].typeName();
                 String b = targetParams[i].typeName();
-                if (!Objects.equals(a, b)) { allMatch = false; break; }
+                if (!typeNamesEqual(a, b)) {
+                    allExactMatch = false;
+                    if (!typeNamesCompatible(a, b)) {
+                        allCompatible = false;
+                        break;
+                    }
+                }
             }
-            if (allMatch) return m;
+            if (allExactMatch) return m;
+            if (allCompatible && compatibleMatch == null) compatibleMatch = m;
         }
-        return null;
+        return compatibleMatch;
+    }
+
+    private static boolean typeNamesEqual(String left, String right) {
+        String a = normalizeTypeName(left);
+        String b = normalizeTypeName(right);
+        return Objects.equals(a, b) || Objects.equals(simpleTypeName(a), 
simpleTypeName(b));
+    }
+
+    private static boolean typeNamesCompatible(String left, String right) {
+        String a = normalizeTypeName(left);
+        String b = normalizeTypeName(right);
+        if (Objects.equals(a, b) || Objects.equals(simpleTypeName(a), 
simpleTypeName(b))) return true;
+        if (isTypeVariableName(a) && isTypeVariableName(b)) return true;
+        if ((isTypeVariableName(a) && isReferenceType(b)) || 
(isTypeVariableName(b) && isReferenceType(a))) {
+            return true;
+        }
+        if (isArrayType(a) && isArrayType(b)) {
+            String leftComponent = arrayComponentType(a);
+            String rightComponent = arrayComponentType(b);
+            if (typeNamesCompatible(leftComponent, rightComponent)) return 
true;
+            if ((isObjectType(leftComponent) && 
isReferenceType(rightComponent))
+                    || (isObjectType(rightComponent) && 
isReferenceType(leftComponent))) {
+                return true;
+            }
+        }
+        return (isObjectType(a) && !isPrimitiveType(b)) || (isObjectType(b) && 
!isPrimitiveType(a));
+    }
+
+    private static String normalizeTypeName(String typeName) {
+        if (typeName == null) return "";
+        String normalized = typeName.replace("...", "[]").trim();
+        int genericStart = normalized.indexOf('<');
+        if (genericStart >= 0) normalized = normalized.substring(0, 
genericStart).trim();
+        if (normalized.startsWith("? extends ")) {
+            normalized = normalized.substring("? extends ".length()).trim();
+        } else if (normalized.startsWith("? super ")) {
+            normalized = normalized.substring("? super ".length()).trim();
+        } else if ("?".equals(normalized)) {
+            normalized = Object.class.getSimpleName();
+        }
+        return normalized;
+    }
+
+    private static String simpleTypeName(String typeName) {
+        int lastDot = typeName.lastIndexOf('.');
+        return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName;
+    }
+
+    private static boolean isObjectType(String typeName) {
+        return "Object".equals(typeName) || 
"java.lang.Object".equals(typeName);
+    }
+
+    private static boolean isArrayType(String typeName) {
+        return typeName.endsWith("[]");
+    }
+
+    private static String arrayComponentType(String typeName) {
+        return typeName.substring(0, typeName.length() - 2);
+    }
+
+    private static boolean isPrimitiveType(String typeName) {
+        return switch (typeName) {
+            case "boolean", "byte", "char", "short", "int", "long", "float", 
"double", "void" -> true;
+            default -> false;
+        };
+    }
+
+    private static boolean isReferenceType(String typeName) {
+        return !isPrimitiveType(typeName);
+    }
+
+    private static boolean isTypeVariableName(String typeName) {
+        return typeName.matches("[A-Z][0-9]?") || "KEY".equals(typeName) || 
"VALUE".equals(typeName);
     }
 
     private static String resolveValueTag(String body, GroovyRootDoc rootDoc, 
SimpleGroovyClassDoc classDoc) {
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/GroovyDocToolTest.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/GroovyDocToolTest.java
index 52a9a9b283..8dd876411e 100644
--- 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/GroovyDocToolTest.java
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/GroovyDocToolTest.java
@@ -823,6 +823,159 @@ public class GroovyDocToolTest extends GroovyTestCase {
                 invokeMethodSection.contains("{@inheritDoc}"));
     }
 
+    public void testInheritDocResolvesFromExternalJdkAbstractClassInHtml() 
throws Exception {
+        String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
+        htmlTool.add(List.of(base + "/JavaExtendsWriterInheritDoc.java"));
+
+        MockOutputTool output = new MockOutputTool();
+        htmlTool.renderToOutput(output, MOCK_DIR);
+
+        String doc = output.getText(MOCK_DIR + "/" + base + 
"/JavaExtendsWriterInheritDoc.html");
+        String closeSection = findMethodSection(doc, "close", "");
+        String flushSection = findMethodSection(doc, "flush", "");
+        assertNotNull("Expected JavaExtendsWriterInheritDoc.html in output", 
doc);
+        assertNotNull("Expected close() section in:\n" + doc, closeSection);
+        assertNotNull("Expected flush() section in:\n" + doc, flushSection);
+        assertTrue("Expected inherited close() text from java.io.Writer in:\n" 
+ doc,
+                normalizeWhitespace(closeSection).contains("Closes the 
stream"));
+        assertTrue("Expected inherited flush() text from java.io.Writer in:\n" 
+ doc,
+                normalizeWhitespace(flushSection).contains("Flushes the 
stream"));
+        assertFalse("External JDK inheritDoc should not remain literal in:\n" 
+ doc,
+                doc.contains("{@inheritDoc}"));
+    }
+
+    public void testInheritDocResolvesFromExternalObjectMethodInHtml() throws 
Exception {
+        String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
+        htmlTool.add(List.of(base + "/JavaObjectCloneInheritDocChild.java"));
+
+        MockOutputTool output = new MockOutputTool();
+        htmlTool.renderToOutput(output, MOCK_DIR);
+
+        String doc = output.getText(MOCK_DIR + "/" + base + 
"/JavaObjectCloneInheritDocChild.html");
+        String cloneSection = findMethodSection(doc, "clone", "");
+        assertNotNull("Expected JavaObjectCloneInheritDocChild.html in 
output", doc);
+        assertNotNull("Expected clone() section in:\n" + doc, cloneSection);
+        assertTrue("Expected inherited clone() text from java.lang.Object 
in:\n" + doc,
+                normalizeWhitespace(cloneSection).contains("Creates and 
returns a copy of this object"));
+        assertFalse("External Object inheritDoc should not remain literal 
in:\n" + doc,
+                doc.contains("{@inheritDoc}"));
+    }
+
+    public void testExternalGroovyClassDocUsesActualSuperclassSemantics() {
+        assertNull("java.lang.Object should not invent a superclass",
+                new ExternalGroovyClassDoc(Object.class).superclass());
+        assertNull("Interfaces should not invent Object as a superclass",
+                new ExternalGroovyClassDoc(Map.class).superclass());
+        assertEquals("Concrete external classes should expose their reflected 
superclass",
+                "java.lang.Object",
+                new 
ExternalGroovyClassDoc(java.io.Writer.class).superclass().qualifiedTypeName());
+    }
+
+    public void 
testExternalJavadocSupportStandaloneLookupDoesNotRetainCaches() {
+        ExternalJavadocSupport.clearCaches();
+
+        GroovyMethodDoc[] docs = ExternalJavadocSupport.methodsFor(new 
ExternalGroovyClassDoc(Map.class));
+        assertTrue("Expected external methods for java.util.Map", docs.length 
> 0);
+
+        ExternalJavadocSupport.CacheStats stats = 
ExternalJavadocSupport.cacheStats();
+        assertEquals("Standalone external lookup should not retain raw comment 
cache entries", 0, stats.rawCommentCacheSize());
+        assertEquals("Standalone external lookup should not retain method 
metadata cache entries", 0, stats.methodCacheSize());
+        assertEquals("Standalone external lookup should not retain method doc 
cache entries", 0, stats.methodDocCacheSize());
+    }
+
+    public void testExternalJavadocSupportClearsCachesWhenSessionCloses() 
throws Exception {
+        ExternalJavadocSupport.clearCaches();
+
+        try (AutoCloseable ignored = 
ExternalJavadocSupport.openCacheSession()) {
+            GroovyMethodDoc[] docs = ExternalJavadocSupport.methodsFor(new 
ExternalGroovyClassDoc(Map.class));
+            assertTrue("Expected external methods for java.util.Map", 
docs.length > 0);
+
+            ExternalJavadocSupport.CacheStats stats = 
ExternalJavadocSupport.cacheStats();
+            assertTrue("Expected raw comment cache entries while the session 
is active", stats.rawCommentCacheSize() > 0);
+            assertTrue("Expected method metadata cache entries while the 
session is active", stats.methodCacheSize() > 0);
+            assertTrue("Expected method doc cache entries while the session is 
active", stats.methodDocCacheSize() > 0);
+        }
+
+        ExternalJavadocSupport.CacheStats stats = 
ExternalJavadocSupport.cacheStats();
+        assertEquals("Raw comment cache should be cleared after the last 
session closes", 0, stats.rawCommentCacheSize());
+        assertEquals("Method metadata cache should be cleared after the last 
session closes", 0, stats.methodCacheSize());
+        assertEquals("Method doc cache should be cleared after the last 
session closes", 0, stats.methodDocCacheSize());
+    }
+
+    public void testInheritDocResolvesFromExternalMapAndObjectMethodsInHtml() 
throws Exception {
+        String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
+        htmlTool.add(List.of(base + "/JavaImplementsMapInheritDoc.java"));
+
+        MockOutputTool output = new MockOutputTool();
+        htmlTool.renderToOutput(output, MOCK_DIR);
+
+        String doc = output.getText(MOCK_DIR + "/" + base + 
"/JavaImplementsMapInheritDoc.html");
+        String clearSection = findMethodSection(doc, "clear", "");
+        String containsValueSection = findMethodSection(doc, "containsValue", 
"java.lang.Object");
+        String equalsSection = findMethodSection(doc, "equals", 
"java.lang.Object");
+        String hashCodeSection = findMethodSection(doc, "hashCode", "");
+        assertNotNull("Expected JavaImplementsMapInheritDoc.html in output", 
doc);
+        assertNotNull("Expected clear() section in:\n" + doc, clearSection);
+        assertNotNull("Expected containsValue(Object) section in:\n" + doc, 
containsValueSection);
+        assertNotNull("Expected equals(Object) section in:\n" + doc, 
equalsSection);
+        assertNotNull("Expected hashCode() section in:\n" + doc, 
hashCodeSection);
+        assertTrue("Expected inherited clear() text from java.util.Map in:\n" 
+ doc,
+                normalizeWhitespace(clearSection).contains("Removes all of the 
mappings from this map"));
+        assertTrue("Expected inherited containsValue(Object) text from 
java.util.Map in:\n" + doc,
+                containsValueSection.contains("Returns <CODE>true</CODE> if 
this map maps one or more keys to the"));
+        assertTrue("Expected inherited equals(Object) text from 
java.lang.Object in:\n" + doc,
+                equalsSection.contains("Indicates whether some other object is 
\"equal to\" this one"));
+        assertTrue("Expected normalized inherited hashCode() text from 
java.lang.Object in:\n" + doc,
+                normalizeWhitespace(hashCodeSection).contains("a hash code 
value for this object"));
+        assertFalse("Inherited external docs should not retain raw Javadoc 
comment markers in:\n" + doc,
+                normalizeWhitespace(doc).contains("* Removes all of the 
mappings"));
+        assertFalse("Inherited external docs should not leave raw link/index 
inline tags in:\n" + doc,
+                doc.contains("{@linkplain") || doc.contains("{@index"));
+        assertFalse("External Map/Object inheritDoc should not remain literal 
in:\n" + doc,
+                doc.contains("{@inheritDoc}"));
+    }
+
+    public void testNestedInternalClassReferencesResolveUsingDocPathNaming() 
throws Exception {
+        String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
+        htmlTool.add(List.of(
+                base + "/JavaNestedResolutionOuter.java",
+                base + "/JavaNestedResolutionSamePackageConsumer.java",
+                base + "/sub/JavaNestedResolutionImportedConsumer.java"));
+
+        MockOutputTool output = new MockOutputTool();
+        htmlTool.renderToOutput(output, MOCK_DIR);
+
+        String samePackageDoc = output.getText(MOCK_DIR + "/" + base + 
"/JavaNestedResolutionSamePackageConsumer.html");
+        String importedDoc = output.getText(MOCK_DIR + "/" + base + 
"/sub/JavaNestedResolutionImportedConsumer.html");
+        String nestedConsumerDoc = output.getText(MOCK_DIR + "/" + base + 
"/JavaNestedResolutionOuter/Enclosing.Consumer.html");
+        assertNotNull("Expected JavaNestedResolutionSamePackageConsumer.html 
in output", samePackageDoc);
+        assertNotNull("Expected JavaNestedResolutionImportedConsumer.html in 
output", importedDoc);
+        assertNotNull("Expected 
JavaNestedResolutionOuter/Enclosing.Consumer.html in output", 
nestedConsumerDoc);
+        assertNotNull("Expected same-package nested helper page in output",
+                output.getText(MOCK_DIR + "/" + base + 
"/JavaNestedResolutionOuter.SamePackageHelper.html"));
+        assertNotNull("Expected imported nested helper page in output",
+                output.getText(MOCK_DIR + "/" + base + 
"/JavaNestedResolutionOuter.ImportedHelper.html"));
+        assertNotNull("Expected sibling nested helper page in output",
+                output.getText(MOCK_DIR + "/" + base + 
"/JavaNestedResolutionOuter/Enclosing.Sibling.html"));
+
+        assertTrue("Same-package nested type should link using dotted doc path 
in:\n" + samePackageDoc,
+                
samePackageDoc.contains("JavaNestedResolutionOuter.SamePackageHelper.html"));
+        assertFalse("Same-package nested type should not use binary-name or 
slash lookup in:\n" + samePackageDoc,
+                
samePackageDoc.contains("JavaNestedResolutionOuter$SamePackageHelper.html")
+                        || 
samePackageDoc.contains("JavaNestedResolutionOuter/SamePackageHelper.html"));
+
+        assertTrue("Imported nested type should link using dotted doc path 
in:\n" + importedDoc,
+                
importedDoc.contains("JavaNestedResolutionOuter.ImportedHelper.html"));
+        assertFalse("Imported nested type should not use binary-name or slash 
lookup in:\n" + importedDoc,
+                
importedDoc.contains("JavaNestedResolutionOuter$ImportedHelper.html")
+                        || 
importedDoc.contains("JavaNestedResolutionOuter/ImportedHelper.html"));
+
+        assertTrue("Nested sibling type should resolve through enclosing types 
using dotted doc path in:\n" + nestedConsumerDoc,
+                nestedConsumerDoc.contains("Enclosing.Sibling.html"));
+        assertFalse("Nested sibling type should not use binary-name lookup 
in:\n" + nestedConsumerDoc,
+                nestedConsumerDoc.contains("Enclosing$Sibling.html"));
+    }
+
     // Cyclic inheritDoc references must collapse safely instead of
     // recursing until the renderer overflows the stack.
     public void testInheritDocCycleDoesNotOverflow() throws Exception {
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaExtendsWriterInheritDoc.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaExtendsWriterInheritDoc.java
new file mode 100644
index 0000000000..50e7d03f06
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaExtendsWriterInheritDoc.java
@@ -0,0 +1,39 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles;
+
+import java.io.IOException;
+import java.io.Writer;
+
+public class JavaExtendsWriterInheritDoc extends Writer {
+    /** {@inheritDoc} */
+    @Override
+    public void write(char[] cbuf, int off, int len) {
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void flush() {
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void close() throws IOException {
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaImplementsMapInheritDoc.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaImplementsMapInheritDoc.java
new file mode 100644
index 0000000000..5caf396250
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaImplementsMapInheritDoc.java
@@ -0,0 +1,112 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class JavaImplementsMapInheritDoc implements Map<String, Object> {
+    private final Map<String, Object> delegate = new LinkedHashMap<String, 
Object>();
+
+    /** {@inheritDoc} */
+    @Override
+    public int size() {
+        return delegate.size();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return delegate.isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean containsKey(Object key) {
+        return delegate.containsKey(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean containsValue(Object value) {
+        return delegate.containsValue(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Object get(Object key) {
+        return delegate.get(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Object put(String key, Object value) {
+        return delegate.put(key, value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Object remove(Object key) {
+        return delegate.remove(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void putAll(Map<? extends String, ? extends Object> m) {
+        delegate.putAll(m);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clear() {
+        delegate.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> keySet() {
+        return delegate.keySet();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<Object> values() {
+        return delegate.values();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        return delegate.entrySet();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(Object obj) {
+        return delegate.equals(obj);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return delegate.hashCode();
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionOuter.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionOuter.java
new file mode 100644
index 0000000000..6423b1edf3
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionOuter.java
@@ -0,0 +1,48 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles;
+
+public class JavaNestedResolutionOuter {
+    /** Same-package nested target. */
+    public static class SamePackageHelper {
+    }
+
+    /** Explicit-import nested target. */
+    public static class ImportedHelper {
+    }
+
+    /** Enclosing nested owner used to resolve sibling nested types. */
+    public static class Enclosing {
+        /** Nested sibling target. */
+        public static class Sibling {
+        }
+
+        /** Nested consumer that references a sibling by its simple source 
name. */
+        public static class Consumer {
+            /**
+             * Returns the sibling helper type.
+             *
+             * @return the sibling helper type
+             */
+            public Sibling sibling() {
+                return null;
+            }
+        }
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionSamePackageConsumer.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionSamePackageConsumer.java
new file mode 100644
index 0000000000..b2622a0490
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaNestedResolutionSamePackageConsumer.java
@@ -0,0 +1,30 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles;
+
+public class JavaNestedResolutionSamePackageConsumer {
+    /**
+     * Returns the same-package nested helper type.
+     *
+     * @return the same-package nested helper type
+     */
+    public JavaNestedResolutionOuter.SamePackageHelper helper() {
+        return null;
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaObjectCloneInheritDocChild.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaObjectCloneInheritDocChild.java
new file mode 100644
index 0000000000..9efd848e99
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaObjectCloneInheritDocChild.java
@@ -0,0 +1,27 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles;
+
+public class JavaObjectCloneInheritDocChild implements Cloneable {
+    /** {@inheritDoc} */
+    @Override
+    public JavaObjectCloneInheritDocChild clone() throws 
CloneNotSupportedException {
+        return (JavaObjectCloneInheritDocChild) super.clone();
+    }
+}
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/sub/JavaNestedResolutionImportedConsumer.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/sub/JavaNestedResolutionImportedConsumer.java
new file mode 100644
index 0000000000..8e86de5aa1
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/sub/JavaNestedResolutionImportedConsumer.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.tools.groovydoc.testfiles.sub;
+
+import org.codehaus.groovy.tools.groovydoc.testfiles.JavaNestedResolutionOuter;
+
+public class JavaNestedResolutionImportedConsumer {
+    /**
+     * Returns the imported nested helper type.
+     *
+     * @return the imported nested helper type
+     */
+    public JavaNestedResolutionOuter.ImportedHelper helper() {
+        return null;
+    }
+}

Reply via email to