Skip to content

Commit 0bad920

Browse files
committed
Refine @inheritDoc support in Groovydoc
1 parent bfd620f commit 0bad920

9 files changed

Lines changed: 246 additions & 28 deletions

File tree

subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/TagRenderer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
* not yet implemented — stage 3 of GROOVY-11938.</li>
7676
* <li>Client-side syntax highlighting assets (Prism.js / highlight.js)
7777
* not yet wired in — stage 4 of GROOVY-11938.</li>
78+
* <li>{@code {@inheritDoc}} inherits the full parent comment rather than
79+
* position-aware substitution inside specific block tags (e.g.
80+
* {@code @param x {@inheritDoc}} inheriting just the parent's
81+
* {@code @param x}). Reasonable initial implementation.</li>
7882
* <li>JEP 467 Markdown reference links — GROOVY-11542, pending.</li>
7983
* </ul>
8084
*

subprojects/groovy-groovydoc/src/main/java/org/codehaus/groovy/tools/groovydoc/antlr4/GroovydocJavaVisitor.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ private List<String> getImports() {
128128
*/
129129
@Override
130130
public void visit(EnumDeclaration n, Object arg) {
131+
if (shouldSkipTypeDeclaration(n)) return;
131132
SimpleGroovyClassDoc parent = visit(n);
132133
currentClassDoc.setTokenType(SimpleGroovyDoc.ENUM_DEF);
133134
super.visit(n, arg);
@@ -161,6 +162,7 @@ public void visit(EnumConstantDeclaration n, Object arg) {
161162
*/
162163
@Override
163164
public void visit(AnnotationDeclaration n, Object arg) {
165+
if (shouldSkipTypeDeclaration(n)) return;
164166
SimpleGroovyClassDoc parent = visit(n);
165167
currentClassDoc.setTokenType(SimpleGroovyDoc.ANNOTATION_DEF);
166168
super.visit(n, arg);
@@ -201,6 +203,7 @@ public void visit(AnnotationMemberDeclaration n, Object arg) {
201203
*/
202204
@Override
203205
public void visit(ClassOrInterfaceDeclaration n, Object arg) {
206+
if (shouldSkipTypeDeclaration(n)) return;
204207
SimpleGroovyClassDoc parent = visit(n);
205208
if (n.isInterface()) {
206209
currentClassDoc.setTokenType(SimpleGroovyDoc.INTERFACE_DEF);
@@ -231,6 +234,7 @@ public void visit(ClassOrInterfaceDeclaration n, Object arg) {
231234
*/
232235
@Override
233236
public void visit(final RecordDeclaration n, final Object arg) {
237+
if (shouldSkipTypeDeclaration(n)) return;
234238
SimpleGroovyClassDoc parent = visit(n);
235239
if (n.isRecordDeclaration()) {
236240
currentClassDoc.setTokenType(SimpleGroovyDoc.RECORD_DEF);
@@ -265,6 +269,10 @@ private String genericTypesAsString(NodeList<TypeParameter> typeParameters) {
265269
return "<" + DefaultGroovyMethods.join(typeParameters, ", ") + ">";
266270
}
267271

272+
private static boolean shouldSkipTypeDeclaration(TypeDeclaration<?> n) {
273+
return !n.isTopLevelType() && !n.isNestedType();
274+
}
275+
268276
private SimpleGroovyClassDoc visit(TypeDeclaration<?> n) {
269277
SimpleGroovyClassDoc parent = null;
270278
List<String> imports = getImports();

subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/GroovyDocToolTest.java

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import groovy.test.GroovyTestCase;
2525
import org.codehaus.groovy.groovydoc.GroovyClassDoc;
2626
import org.codehaus.groovy.groovydoc.GroovyMethodDoc;
27+
import org.codehaus.groovy.groovydoc.GroovyParameter;
2728
import org.codehaus.groovy.groovydoc.GroovyRootDoc;
2829
import org.codehaus.groovy.runtime.StringGroovyMethods;
2930
import org.codehaus.groovy.tools.groovydoc.antlr4.GroovyDocParser;
@@ -512,7 +513,7 @@ public void testInheritDocSubstitutesMatchingParentBlockTagsInGroovy() throws Ex
512513

513514
public void testInheritDocSubstitutesMatchingParentBlockTagsInJava() throws Exception {
514515
String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
515-
htmlTool.add(List.of(base + "/JavaInheritDocTagChild.java"));
516+
htmlTool.add(List.of(base + "/JavaInheritDocTagBase.java", base + "/JavaInheritDocTagChild.java"));
516517

517518
MockOutputTool output = new MockOutputTool();
518519
htmlTool.renderToOutput(output, MOCK_DIR);
@@ -559,7 +560,7 @@ public void testInheritDocPreservesSurroundingTextAndExceptionTagsInGroovy() thr
559560

560561
public void testInheritDocPreservesSurroundingTextAndExceptionTagsInJava() throws Exception {
561562
String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
562-
htmlTool.add(List.of(base + "/JavaInheritDocRichTagChild.java"));
563+
htmlTool.add(List.of(base + "/JavaInheritDocRichTagBase.java", base + "/JavaInheritDocRichTagChild.java"));
563564

564565
MockOutputTool output = new MockOutputTool();
565566
htmlTool.renderToOutput(output, MOCK_DIR);
@@ -583,6 +584,63 @@ public void testInheritDocPreservesSurroundingTextAndExceptionTagsInJava() throw
583584
doc.contains("{@inheritDoc}"));
584585
}
585586

587+
public void testInheritDocResolvesForJavaLocalClassRegressionAtRootDocLevel() throws Exception {
588+
String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
589+
htmlTool.add(List.of(base + "/JavaLocalClassInheritDocBase.java", base + "/JavaLocalClassInheritDocChild.java"));
590+
591+
SimpleGroovyRootDoc rootDoc = (SimpleGroovyRootDoc) htmlTool.getRootDoc();
592+
GroovyClassDoc childClass = rootDoc.classNamedExact(base + "/JavaLocalClassInheritDocChild");
593+
GroovyClassDoc baseClass = rootDoc.classNamedExact(base + "/JavaLocalClassInheritDocBase");
594+
assertNotNull("Expected JavaLocalClassInheritDocChild in root doc", childClass);
595+
assertNotNull("Expected JavaLocalClassInheritDocBase in root doc", baseClass);
596+
assertNull("Local helper class LocalMixinMethodTracker should not become a top-level doc node",
597+
rootDoc.classNamedExact(base + "/LocalMixinMethodTracker"));
598+
assertEquals("Expected JavaLocalClassInheritDocChild to resolve JavaLocalClassInheritDocBase as its superclass",
599+
base + "/JavaLocalClassInheritDocBase", childClass.superclass().getFullPathName());
600+
601+
GroovyMethodDoc child = findMethod(childClass, "findMixinMethod", 2);
602+
GroovyMethodDoc parent = findMethod(baseClass, "findMixinMethod", 2);
603+
assertNotNull("Expected JavaLocalClassInheritDocChild.findMixinMethod in root doc. Available candidates: "
604+
+ describeMethods(childClass, "findMixinMethod"), child);
605+
assertNotNull("Expected JavaLocalClassInheritDocBase.findMixinMethod in root doc. Available candidates: "
606+
+ describeMethods(baseClass, "findMixinMethod") + ". Anywhere in root: "
607+
+ describeClassesWithMethod(rootDoc, "findMixinMethod"), parent);
608+
609+
String childComment = normalizeWhitespace(child.commentText());
610+
assertTrue("Expected inherited first sentence on JavaLocalClassInheritDocChild.findMixinMethod.\nchild="
611+
+ describeMethod(child) + "\nparent=" + describeMethod(parent) + "\ncomment=" + child.commentText(),
612+
childComment.contains("Searches for a matching mixin method."));
613+
assertTrue("Expected inherited return description on JavaLocalClassInheritDocChild.findMixinMethod.\nchild="
614+
+ describeMethod(child) + "\nparent=" + describeMethod(parent) + "\ncomment=" + child.commentText(),
615+
childComment.contains("the matching mixin method, or <CODE>null</CODE> if none is found"));
616+
assertFalse("JavaLocalClassInheritDocChild.findMixinMethod should not keep literal inheritDoc in commentText.\nchild="
617+
+ describeMethod(child) + "\nparent=" + describeMethod(parent) + "\ncomment=" + child.commentText(),
618+
child.commentText().contains("{@inheritDoc}"));
619+
}
620+
621+
public void testInheritDocResolvesForJavaLocalClassRegressionInHtml() throws Exception {
622+
String base = "org/codehaus/groovy/tools/groovydoc/testfiles";
623+
htmlTool.add(List.of(base + "/JavaLocalClassInheritDocBase.java", base + "/JavaLocalClassInheritDocChild.java"));
624+
MockOutputTool output = new MockOutputTool();
625+
htmlTool.renderToOutput(output, MOCK_DIR);
626+
627+
String doc = output.getText(MOCK_DIR + "/" + base + "/JavaLocalClassInheritDocChild.html");
628+
String methodSection = substringBetween(doc,
629+
"<a name=\"findMixinMethod(java.lang.String, java.lang.Class)\"><!-- --></a>",
630+
"<!-- ========= END OF CLASS DATA ========= -->");
631+
String normalized = normalizeWhitespace(methodSection);
632+
assertNotNull("Expected JavaLocalClassInheritDocChild.html in output", doc);
633+
assertNull("Local helper class LocalMixinMethodTracker should not get its own HTML page",
634+
output.getText(MOCK_DIR + "/" + base + "/LocalMixinMethodTracker.html"));
635+
assertNotNull("Expected a dedicated HTML section for JavaLocalClassInheritDocChild.findMixinMethod:\n" + doc, methodSection);
636+
assertTrue("Expected inherited summary in JavaLocalClassInheritDocChild.findMixinMethod HTML:\n" + doc,
637+
normalized.contains("Searches for a matching mixin method."));
638+
assertTrue("Expected inherited return text in JavaLocalClassInheritDocChild.findMixinMethod HTML:\n" + doc,
639+
normalized.contains("the matching mixin method, or <CODE>null</CODE> if none is found"));
640+
assertFalse("JavaLocalClassInheritDocChild.findMixinMethod HTML should not keep literal inheritDoc:\n" + doc,
641+
methodSection.contains("{@inheritDoc}"));
642+
}
643+
586644
// Cyclic inheritDoc references must collapse safely instead of
587645
// recursing until the renderer overflows the stack.
588646
public void testInheritDocCycleDoesNotOverflow() throws Exception {
@@ -658,6 +716,46 @@ private static String normalizeWhitespace(String text) {
658716
return text == null ? null : text.replaceAll("\\s+", " ").trim();
659717
}
660718

719+
private static GroovyMethodDoc findMethod(GroovyClassDoc classDoc, String name, int parameterCount) {
720+
for (GroovyMethodDoc method : classDoc.methods()) {
721+
if (name.equals(method.name()) && method.parameters().length == parameterCount) {
722+
return method;
723+
}
724+
}
725+
return null;
726+
}
727+
728+
private static String describeMethod(GroovyMethodDoc method) {
729+
if (method == null) return "null";
730+
String owner = method.containingClass() == null ? "<no-owner>" : method.containingClass().qualifiedTypeName();
731+
return owner + "#" + method.name() + "("
732+
+ Arrays.stream(method.parameters()).map(GroovyParameter::typeName).collect(Collectors.joining(", "))
733+
+ ")";
734+
}
735+
736+
private static String describeMethods(GroovyClassDoc classDoc, String name) {
737+
return Arrays.stream(classDoc.methods())
738+
.filter(method -> name.equals(method.name()))
739+
.map(GroovyDocToolTest::describeMethod)
740+
.collect(Collectors.joining("; "));
741+
}
742+
743+
private static String describeClassesWithMethod(GroovyRootDoc rootDoc, String name) {
744+
return Arrays.stream(rootDoc.classes())
745+
.map(classDoc -> classDoc.getFullPathName() + ": " + describeMethods(classDoc, name))
746+
.filter(s -> !s.endsWith(": "))
747+
.collect(Collectors.joining(" | "));
748+
}
749+
750+
private static String substringBetween(String text, String startMarker, String endMarker) {
751+
if (text == null) return null;
752+
int start = text.indexOf(startMarker);
753+
if (start < 0) return null;
754+
int end = text.indexOf(endMarker, start + startMarker.length());
755+
if (end < 0) return text.substring(start);
756+
return text.substring(start, end);
757+
}
758+
661759
// GROOVY-8025: annotations whose members are closure expressions must
662760
// not NPE during groovydoc processing. The original bug was in the old
663761
// SimpleGroovyClassDocAssembler — the refactor to GroovydocVisitor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.tools.groovydoc.testfiles;
20+
21+
import java.io.IOException;
22+
23+
class JavaInheritDocRichTagBase {
24+
/**
25+
* Java base transform description that should not leak into block tags.
26+
*
27+
* @param value java parent parameter description with {@code JAVA_VALUE_TOKEN}
28+
* @return java parent return description with {@code JAVA_RETURN_TOKEN}
29+
* @throws IllegalArgumentException java parent illegal-argument description with {@code JAVA_BAD_VALUE_TOKEN}
30+
* @exception java.io.IOException java parent IO description with {@code JAVA_IO_VALUE_TOKEN}
31+
*/
32+
public String transform(String value) throws IOException {
33+
return value.toUpperCase();
34+
}
35+
}

subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaInheritDocRichTagChild.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,6 @@
2020

2121
import java.io.IOException;
2222

23-
class JavaInheritDocRichTagBase {
24-
/**
25-
* Java base transform description that should not leak into block tags.
26-
*
27-
* @param value java parent parameter description with {@code JAVA_VALUE_TOKEN}
28-
* @return java parent return description with {@code JAVA_RETURN_TOKEN}
29-
* @throws IllegalArgumentException java parent illegal-argument description with {@code JAVA_BAD_VALUE_TOKEN}
30-
* @exception java.io.IOException java parent IO description with {@code JAVA_IO_VALUE_TOKEN}
31-
*/
32-
public String transform(String value) throws IOException {
33-
return value.toUpperCase();
34-
}
35-
}
36-
3723
public class JavaInheritDocRichTagChild extends JavaInheritDocRichTagBase {
3824
/**
3925
* Java rich child summary.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.tools.groovydoc.testfiles;
20+
21+
class JavaInheritDocTagBase {
22+
/**
23+
* Java base transform description.
24+
*
25+
* @param value java parent parameter description
26+
* @return java parent return description
27+
*/
28+
public String transform(String value) {
29+
return value.toUpperCase();
30+
}
31+
}

subprojects/groovy-groovydoc/src/test/groovy/org/codehaus/groovy/tools/groovydoc/testfiles/JavaInheritDocTagChild.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,6 @@
1818
*/
1919
package org.codehaus.groovy.tools.groovydoc.testfiles;
2020

21-
class JavaInheritDocTagBase {
22-
/**
23-
* Java base transform description.
24-
*
25-
* @param value java parent parameter description
26-
* @return java parent return description
27-
*/
28-
public String transform(String value) {
29-
return value.toUpperCase();
30-
}
31-
}
32-
3321
public class JavaInheritDocTagChild extends JavaInheritDocTagBase {
3422
/**
3523
* Java child summary.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.tools.groovydoc.testfiles;
20+
21+
class JavaLocalClassInheritDocBase {
22+
void prepareMixinMethods() {
23+
class LocalMixinMethodTracker {
24+
void record() {
25+
}
26+
}
27+
28+
new LocalMixinMethodTracker().record();
29+
}
30+
31+
/**
32+
* Searches for a matching mixin method.
33+
*
34+
* @param methodName the method name
35+
* @param arguments the parameter types
36+
* @return the matching mixin method, or {@code null} if none is found
37+
*/
38+
protected Object findMixinMethod(String methodName, Class[] arguments) {
39+
return null;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.tools.groovydoc.testfiles;
20+
21+
public class JavaLocalClassInheritDocChild extends JavaLocalClassInheritDocBase {
22+
/** {@inheritDoc} */
23+
@Override
24+
public Object findMixinMethod(String methodName, Class[] arguments) {
25+
return null;
26+
}
27+
}

0 commit comments

Comments
 (0)