2424import groovy .test .GroovyTestCase ;
2525import org .codehaus .groovy .groovydoc .GroovyClassDoc ;
2626import org .codehaus .groovy .groovydoc .GroovyMethodDoc ;
27+ import org .codehaus .groovy .groovydoc .GroovyParameter ;
2728import org .codehaus .groovy .groovydoc .GroovyRootDoc ;
2829import org .codehaus .groovy .runtime .StringGroovyMethods ;
2930import 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.\n child="
611+ + describeMethod (child ) + "\n parent=" + describeMethod (parent ) + "\n comment=" + child .commentText (),
612+ childComment .contains ("Searches for a matching mixin method." ));
613+ assertTrue ("Expected inherited return description on JavaLocalClassInheritDocChild.findMixinMethod.\n child="
614+ + describeMethod (child ) + "\n parent=" + describeMethod (parent ) + "\n comment=" + 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.\n child="
617+ + describeMethod (child ) + "\n parent=" + describeMethod (parent ) + "\n comment=" + 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
0 commit comments