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();
+ }
+ }
+}