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

asf-gitbox-commits 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 43a931fb03 Enhance type resolution for groovydoc
43a931fb03 is described below

commit 43a931fb038649f73bf77ee01aeb718e061a6927
Author: Daniel Sun <[email protected]>
AuthorDate: Mon May 4 16:36:45 2026 +0900

    Enhance type resolution for groovydoc
---
 .../tools/groovydoc/SimpleGroovyClassDoc.java      | 38 +++++++++++---
 .../groovy/tools/groovydoc/GroovyDocToolTest.java  | 23 ++++++++
 .../groovydoc/testfiles/SimulatedScopedLocal.java  | 61 ++++++++++++++++++++++
 3 files changed, 116 insertions(+), 6 deletions(-)

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 fc2a4343b9..7a9cfd5ac5 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
@@ -665,20 +665,32 @@ public class SimpleGroovyClassDoc extends 
SimpleGroovyAbstractableElementDoc imp
         if (!type.contains(".") && classDoc != null) {
             String[] pieces = type.split("#", -1);
             String candidate = pieces[0];
-            Class c = classDoc.resolveExternalClassFromImport(candidate);
-            if (c != null) type = c.getName();
+            GroovyClassDoc resolvedDoc = resolveInternalShortName(rootDoc, 
classDoc, candidate);
+            if (resolvedDoc != null) {
+                type = resolvedDoc.getFullPathName();
+            } else {
+                Class c = classDoc.resolveExternalClassFromImport(candidate);
+                if (c != null) type = c.getName();
+            }
             if (pieces.length > 1) type += "#" + pieces[1];
             type = resolveMethodArgs(rootDoc, classDoc, type);
         }
 
         final String[] target = type.split("#");
-        String shortClassName = target[0].replaceAll(".*\\.", "");
+        String shortClassName = target[0];
+        int lastSlash = shortClassName.lastIndexOf('/');
+        if (lastSlash >= 0) {
+            shortClassName = shortClassName.substring(lastSlash + 1);
+        }
+        shortClassName = shortClassName.replaceAll(".*\\.", "");
         shortClassName += (target.length > 1 ? "#" + target[1].split("\\(", 
-1)[0] : "");
-        String name = (full ? target[0] : shortClassName).replace('#', 
'.').replace('$', '.');
+        String name = (full ? target[0] : shortClassName).replace('/', 
'.').replace('#', '.').replace('$', '.');
 
         // last chance lookup for classes within the current codebase
         if (rootDoc != null) {
-            String slashedName = target[0].replace('.', '/');
+            String slashedName = target[0].contains("/")
+                    ? target[0].replace('$', '.')
+                    : target[0].replace('.', '/');
             GroovyClassDoc doc = rootDoc.classNamed(classDoc, slashedName);
             if (doc != null) {
                 target[0] = doc.getFullPathName(); // if we added a package
@@ -702,11 +714,25 @@ public class SimpleGroovyClassDoc extends 
SimpleGroovyAbstractableElementDoc imp
         return type;
     }
 
+    private static GroovyClassDoc resolveInternalShortName(GroovyRootDoc 
rootDoc, SimpleGroovyClassDoc classDoc, String candidate) {
+        if (rootDoc == null) return null;
+        GroovyClassDoc resolvedDoc = classDoc.resolveClass(rootDoc, candidate);
+        if (!(resolvedDoc instanceof SimpleGroovyClassDoc)) return null;
+
+        String fullPathName = resolvedDoc.getFullPathName();
+        if (fullPathName == null || fullPathName.equals(candidate)) return 
null;
+
+        return resolvedDoc;
+    }
+
     private static String buildUrl(String relativeRoot, String[] target, 
String shortClassName) {
         if (!relativeRoot.isEmpty() && !relativeRoot.endsWith("/")) {
             relativeRoot += "/";
         }
-        String url = relativeRoot + target[0].replace('.', '/').replace('$', 
'.') + ".html" + (target.length > 1 ? "#" + target[1] : "");
+        String targetPath = target[0].contains("/")
+                ? target[0].replace('$', '.')
+                : target[0].replace('.', '/').replace('$', '.');
+        String url = relativeRoot + targetPath + ".html" + (target.length > 1 
? "#" + target[1] : "");
         return "<a href='" + url + "' title='" + shortClassName + "'>" + 
shortClassName + "</a>";
     }
 
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 103088b899..df2a5e2c6f 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
@@ -1507,6 +1507,29 @@ public class GroovyDocToolTest extends GroovyTestCase {
                         && 
helperPage.contains(">ScriptWithSiblingClassLinks.method</a>"));
     }
 
+    public void 
testSimulatedScopedLocalNestedCarrierLinksRenderAsWorkingAnchors() throws 
Exception {
+        String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
+        htmlTool.add(List.of(base + "/SimulatedScopedLocal.java"));
+
+        MockOutputTool output = new MockOutputTool();
+        htmlTool.renderToOutput(output, MOCK_DIR);
+
+        String page = output.getText(MOCK_DIR + "/" + base + 
"/SimulatedScopedLocal.html");
+        assertNotNull("Expected a page for SimulatedScopedLocal", page);
+        assertTrue("SimulatedScopedLocal page should link Carrier.run in:\n" + 
page,
+                page.contains("SimulatedScopedLocal.Carrier.html#run(")
+                        && page.contains(">Carrier.run</a>"));
+        assertTrue("SimulatedScopedLocal page should link Carrier.call in:\n" 
+ page,
+                page.contains("SimulatedScopedLocal.Carrier.html#call(")
+                        && page.contains(">Carrier.call</a>"));
+        assertFalse("SimulatedScopedLocal page should not duplicate the raw 
Carrier.run reference in:\n" + page,
+                page.contains("Carrier#run(Runnable)#run(Runnable)"));
+        assertFalse("SimulatedScopedLocal page should not duplicate the raw 
Carrier.call reference in:\n" + page,
+                page.contains("Carrier#call(Supplier)#call(Supplier)"));
+        assertFalse("SimulatedScopedLocal page should not point nested Carrier 
links at a slash-based path in:\n" + page,
+                page.contains("SimulatedScopedLocal/Carrier.html#run(") || 
page.contains("SimulatedScopedLocal/Carrier.html#call("));
+    }
+
     // GROOVY-11943: -noindex / -nodeprecatedlist / -nohelp skip the matching
     // auxiliary top-level page AND suppress its nav-bar link on every page
     // that would otherwise reference it.
diff --git 
a/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/SimulatedScopedLocal.java
 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/SimulatedScopedLocal.java
new file mode 100644
index 0000000000..8f6d81a56a
--- /dev/null
+++ 
b/subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/SimulatedScopedLocal.java
@@ -0,0 +1,61 @@
+/*
+ *  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.function.Supplier;
+
+/**
+ * A test fixture that mimics the nested-carrier link shape used by 
ScopedLocal.
+ *
+ * <ul>
+ *   <li>{@link Carrier#run(Runnable)}, {@link Carrier#call(Supplier)} -
+ *       execute code with the bindings active.</li>
+ * </ul>
+ */
+public final class SimulatedScopedLocal {
+
+    private SimulatedScopedLocal() {
+    }
+
+    /**
+     * Creates a {@link Carrier} that binds {@code key} to {@code value}.
+     * The binding takes effect when {@link Carrier#run(Runnable)} or
+     * {@link Carrier#call(Supplier)} is invoked.
+     *
+     * @param key the scoped-local to bind
+     * @param value the value to bind
+     * @return a carrier holding the binding
+     */
+    public static Carrier where(Object key, Object value) {
+        return new Carrier();
+    }
+
+    /**
+     * Simple nested carrier used only for groovydoc regression coverage.
+     */
+    public static final class Carrier {
+        public void run(Runnable action) {
+            action.run();
+        }
+
+        public <T> T call(Supplier<T> supplier) {
+            return supplier.get();
+        }
+    }
+}

Reply via email to