Skip to content

Commit 7e0e27d

Browse files
authored
Resolves #61. Inherit missing javadoc components from overridden methods (#64)
Closes #61. Inherit missing javadoc components from overridden methods if they exist. This adds javadoc from the overridden method in super class or interfaces if none is present on the declared method at runtime. When a class is extended and/or multiple interfaces are implemented with the same signature priority returned by the super-class first then the interfaces in the order they are declared as is done by the javadoc command line tool One of the potentially main issues I was not able to figure out was how to preserve the parameter order when inheriting java doc when some of the params are on the overriding method and some on the overridden. We don't have guaranteed access to the param names at runtime which makes it difficult to discern true order. Currently the inherited params are just added to the end of the list. If the java doc is not complete then the missing parts are inherited from the overridden methods as described here https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html. When loading a class javadoc all the methods are properly populated with inheritance. When loading just method javadoc only the necessary javadoc are loaded. I changed the class javadoc to use a map in order to have more efficient method lookup. I left the getters to return list for the various components and the order is preserved from how they are inserted. It may be better to at some point change the return type to a collection as it can be a strict view, but at the moment this was not done in case dependent code expects a list. Also something to consider is that in the annotation processor we erase all the type parameters so the param types of methods are limited to their bounds which can be seen on the generic method in Documented Class. However the javadoc tool actually preserves the generic type and bounds. This did not cover the case of adding the javadoc from protected fields or parsing @ inheritdoc as I think these are a separate concern. Note this implementation does not require @ Override to be present on the method. Note that when you generate javadoc for the VeryComplexImplementation it actually does not follow the algorithm provided [in the link.](https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html. It actually performs recursive search on the superclass first resulting in inheriting the javadoc for the fling method from DocumentedInterface rather than CompetingInterface as would be expected if the algorithm ran as described. So likely the order priority is something that changes with java versions unfortunately
1 parent daea64a commit 7e0e27d

18 files changed

+1029
-183
lines changed

Diff for: therapi-runtime-javadoc-scribe/src/test/java/com/github/therapi/runtimejavadoc/JavadocAnnotationProcessorTest.java

+266
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.github.therapi.runtimejavadoc.scribe.JavadocAnnotationProcessor;
44
import com.google.testing.compile.Compilation;
55
import com.google.testing.compile.JavaFileObjects;
6+
import java.util.Arrays;
7+
import static org.junit.Assert.assertFalse;
68
import org.junit.Test;
79

810
import javax.tools.JavaFileObject;
@@ -26,6 +28,14 @@ public class JavadocAnnotationProcessorTest {
2628
private static final String DOCUMENTED_CLASS = "javasource.foo.DocumentedClass";
2729
private static final String DOCUMENTED_ENUM = "javasource.foo.DocumentedEnum";
2830
private static final String COMPLEX_ENUM = "javasource.foo.ComplexEnum";
31+
private static final String OVERRIDING_CLASS_IN_ANOTHER_PACKAGE = "javasource.bar.OverridingClassInAnotherPackage";
32+
private static final String OVERRIDING_CLASS = "javasource.foo.OverridingClass";
33+
private static final String OVERRIDING_CLASS_2_DEGREES = "javasource.foo.OverridingClass2Degrees";
34+
private static final String OTHER_INTERFACE = "javasource.foo.OtherInterface";
35+
private static final String DOCUMENTED_INTERFACE = "javasource.foo.DocumentedInterface";
36+
private static final String DOCUMENTED_IMPLEMENTATION = "javasource.foo.DocumentedImplementation";
37+
private static final String COMPLEX_IMPLEMENTATION = "javasource.foo.ComplexImplementation";
38+
private static final String VERY_COMPLEX_IMPLEMENTATION = "javasource.foo.VeryComplexImplementation";
2939
private static final String ANOTHER_DOCUMENTED_CLASS = "javasource.bar.AnotherDocumentedClass";
3040
private static final String ANNOTATED_WITH_RETAIN_JAVADOC = "javasource.bar.YetAnotherDocumentedClass";
3141
private static final String UNDOCUMENTED = "javasource.bar.UndocumentedClass";
@@ -41,6 +51,14 @@ private static List<JavaFileObject> sources() {
4151
"javasource/foo/DocumentedClass.java",
4252
"javasource/foo/DocumentedEnum.java",
4353
"javasource/foo/ComplexEnum.java",
54+
"javasource/foo/OverridingClass.java",
55+
"javasource/foo/OverridingClass2Degrees.java",
56+
"javasource/foo/DocumentedInterface.java",
57+
"javasource/foo/DocumentedImplementation.java",
58+
"javasource/foo/ComplexImplementation.java",
59+
"javasource/foo/VeryComplexImplementation.java",
60+
"javasource/foo/OtherInterface.java",
61+
"javasource/bar/OverridingClassInAnotherPackage.java",
4462
"javasource/bar/AnotherDocumentedClass.java",
4563
"javasource/bar/YetAnotherDocumentedClass.java",
4664
"javasource/bar/UndocumentedClass.java",
@@ -80,6 +98,17 @@ public void classNameIsPreserved() throws Exception {
8098
}
8199
}
82100

101+
@Test
102+
public void methodsFullyPopulatedByDefault() throws Exception {
103+
try (CompilationClassLoader classLoader = compile(null)) {
104+
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);
105+
ClassJavadoc classJavadoc = expectJavadoc(c);
106+
107+
Method m = c.getMethod("frobulate", String.class, List.class);
108+
assertFalse(classJavadoc.findMatchingMethod(m).isEmpty());
109+
}
110+
}
111+
83112
@Test
84113
public void retainFromAllPackages() throws Exception {
85114
try (CompilationClassLoader classLoader = compile(null)) {
@@ -231,6 +260,228 @@ public void methodsMatchDespiteOverload() throws Exception {
231260

232261
assertMethodMatches(m1, "Frobulate <code>a</code> by <code>b</code>");
233262
assertMethodMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");
263+
264+
Method m3 = c.getDeclaredMethod("equals", Object.class);
265+
266+
// javadoc tools do not inherit javadoc from Object
267+
expectNoJavadoc(m3);
268+
}
269+
}
270+
271+
@Test
272+
public void methodsMatchDespiteExtendingFromAnotherPackage() throws Exception {
273+
try (CompilationClassLoader classLoader = compile(null)) {
274+
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS_IN_ANOTHER_PACKAGE);
275+
276+
final String methodName = "frobulate";
277+
Method m1 = c.getDeclaredMethod(methodName, String.class, int.class);
278+
Method m2 = c.getDeclaredMethod(methodName, String.class, List.class);
279+
280+
assertMethodDescriptionMatches(m1, "Quick frobulate <code>a</code> by <code>b</code> using thin frobulation");
281+
assertMethodDescriptionMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");
282+
}
283+
}
284+
285+
@Test
286+
public void methodsMatchWithExtendedClass() throws Exception {
287+
try (CompilationClassLoader classLoader = compile(null)) {
288+
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);
289+
290+
final String methodName = "frobulate";
291+
Method m1 = c.getDeclaredMethod(methodName, String.class, int.class);
292+
293+
MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
294+
assertEquals(m1.getName(), methodJavadoc1.getName());
295+
296+
String actualDesc = formatter.format(methodJavadoc1.getComment());
297+
assertEquals("Super frobulate <code>a</code> by <code>b</code> using extended frobulation", actualDesc);
298+
assertEquals(2, methodJavadoc1.getParams().size());
299+
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
300+
assertFalse(methodJavadoc1.getThrows().isEmpty());
301+
302+
Method m2 = c.getDeclaredMethod(methodName, String.class, List.class);
303+
assertMethodDescriptionMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");
304+
}
305+
}
306+
307+
@Test
308+
public void methodsMatchWithExtendedClass2Degrees() throws Exception {
309+
try (CompilationClassLoader classLoader = compile(null)) {
310+
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS_2_DEGREES);
311+
312+
final String methodName = "skipMethod";
313+
Method m1 = c.getDeclaredMethod(methodName);
314+
315+
MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
316+
assertEquals(m1.getName(), methodJavadoc1.getName());
317+
318+
String actualDesc = formatter.format(methodJavadoc1.getComment());
319+
assertEquals("I am also a simple method", actualDesc);
320+
assertFalse(methodJavadoc1.getThrows().isEmpty());
321+
}
322+
}
323+
324+
@Test
325+
public void genericMethodsMatchWithExtendedClass() throws Exception {
326+
try (CompilationClassLoader classLoader = compile(null)) {
327+
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);
328+
329+
final String methodName1 = "genericMethod";
330+
Method m1 = c.getDeclaredMethod(methodName1, String.class);
331+
332+
assertMethodDescriptionMatches(m1, "Generic method to do generic things");
333+
334+
final String methodName2 = "separateGeneric";
335+
Method m2 = c.getDeclaredMethod(methodName2, Integer.class);
336+
337+
expectNoJavadoc(m2);
338+
}
339+
}
340+
341+
@Test
342+
public void genericMethodsMatchWithClass() throws Exception {
343+
try (CompilationClassLoader classLoader = compile(null)) {
344+
Class<?> c = classLoader.loadClass(DOCUMENTED_CLASS);
345+
346+
final String methodName1 = "genericMethod";
347+
Method m1 = c.getDeclaredMethod(methodName1, Object.class);
348+
349+
assertMethodDescriptionMatches(m1, "Generic method to do generic things");
350+
351+
final String methodName2 = "separateGeneric";
352+
Method m2 = c.getDeclaredMethod(methodName2, Comparable.class);
353+
354+
assertMethodDescriptionMatches(m2, "Generic method to do other things");
355+
}
356+
}
357+
358+
@Test
359+
public void methodsMatchWithImplementation() throws Exception {
360+
try (CompilationClassLoader classLoader = compile(null)) {
361+
Class<?> c = classLoader.loadClass(DOCUMENTED_IMPLEMENTATION);
362+
363+
final String methodName = "hoodwink";
364+
Method m1 = c.getDeclaredMethod(methodName, String.class);
365+
366+
MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
367+
assertEquals(m1.getName(), methodJavadoc1.getName());
368+
369+
String actualDesc = formatter.format(methodJavadoc1.getComment());
370+
assertEquals("hoodwink a stranger", actualDesc);
371+
assertEquals(1, methodJavadoc1.getParams().size());
372+
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
373+
assertFalse(methodJavadoc1.getThrows().isEmpty());
374+
375+
final String methodName2 = "snaggle";
376+
Method m2 = c.getDeclaredMethod(methodName2, String.class);
377+
assertMethodDescriptionMatches(m2, "Snaggle a kerfluffin");
378+
}
379+
}
380+
381+
@Test
382+
public void genericMethodsMatchWithImplementation() throws Exception {
383+
try (CompilationClassLoader classLoader = compile(null)) {
384+
Class<?> c = classLoader.loadClass(DOCUMENTED_IMPLEMENTATION);
385+
386+
final String methodName1 = "fling";
387+
388+
Method m1 = c.getDeclaredMethod(methodName1, Integer.class);
389+
390+
MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
391+
assertEquals(m1.getName(), methodJavadoc1.getName());
392+
String actualDesc = formatter.format(methodJavadoc1.getComment());
393+
assertEquals("Fling the tea", actualDesc);
394+
assertEquals(1, methodJavadoc1.getParams().size());
395+
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
396+
assertFalse(methodJavadoc1.getThrows().isEmpty());
397+
assertEquals(methodJavadoc1.getParamTypes(), Arrays.asList("java.lang.Integer"));
398+
assertEquals("the tea weight", formatter.format(methodJavadoc1.getParams().get(0).getComment()));
399+
400+
Method m2 = c.getDeclaredMethod(methodName1, Object.class);
401+
expectNoJavadoc(m2);
402+
}
403+
}
404+
405+
@Test
406+
public void methodsMatchOnInterface() throws Exception {
407+
try (CompilationClassLoader classLoader = compile(null)) {
408+
Class<?> c1 = classLoader.loadClass(DOCUMENTED_INTERFACE);
409+
Class<?> c2 = classLoader.loadClass(OTHER_INTERFACE);
410+
411+
final String methodName1 = "hoodwink";
412+
Method m1 = c1.getDeclaredMethod(methodName1, String.class);
413+
Method m2 = c2.getDeclaredMethod(methodName1, String.class);
414+
415+
assertMethodDescriptionMatches(m1, "Hoodwink a kerfluffin");
416+
assertMethodDescriptionMatches(m2, "Hoodwink a schmadragon");
417+
418+
final String methodName2 = "snaggle";
419+
Method m3 = c1.getDeclaredMethod(methodName2, String.class);
420+
assertMethodDescriptionMatches(m3, "Snaggle a kerfluffin");
421+
}
422+
}
423+
424+
@Test
425+
public void genericMethodsMatchOnInterface() throws Exception {
426+
try (CompilationClassLoader classLoader = compile(null)) {
427+
Class<?> c1 = classLoader.loadClass(DOCUMENTED_INTERFACE);
428+
Class<?> c2 = classLoader.loadClass(OTHER_INTERFACE);
429+
430+
final String methodName3 = "fling";
431+
Method m4 = c1.getDeclaredMethod(methodName3, Number.class);
432+
Method m5 = c2.getDeclaredMethod(methodName3, Number.class);
433+
assertMethodDescriptionMatches(m4, "Fling the tea");
434+
assertMethodDescriptionMatches(m5, "Fling the vorrdin");
435+
}
436+
}
437+
438+
@Test
439+
public void methodsMatchOnMultipleImplementedInterface() throws Exception {
440+
try (CompilationClassLoader classLoader = compile(null)) {
441+
Class<?> c1 = classLoader.loadClass(COMPLEX_IMPLEMENTATION);
442+
443+
final String methodName1 = "hoodwink";
444+
Method m1 = c1.getDeclaredMethod(methodName1, String.class);
445+
446+
assertMethodDescriptionMatches(m1, "Hoodwink a kerfluffin");
447+
448+
final String methodName2 = "snaggle";
449+
Method m2 = c1.getDeclaredMethod(methodName2, String.class);
450+
assertMethodDescriptionMatches(m2, "Snaggle a kerfluffin");
451+
}
452+
}
453+
454+
@Test
455+
public void genericMethodsMatchOnMultipleImplementedInterface() throws Exception {
456+
try (CompilationClassLoader classLoader = compile(null)) {
457+
Class<?> c1 = classLoader.loadClass(COMPLEX_IMPLEMENTATION);
458+
459+
final String methodName3 = "fling";
460+
Method m3 = c1.getDeclaredMethod(methodName3, Integer.class);
461+
assertMethodDescriptionMatches(m3, "Fling the tea");
462+
}
463+
}
464+
465+
@Test
466+
public void methodsMatchOnExtendedClassAndImplementedInterface() throws Exception {
467+
try (CompilationClassLoader classLoader = compile(null)) {
468+
Class<?> c1 = classLoader.loadClass(VERY_COMPLEX_IMPLEMENTATION);
469+
470+
final String methodName1 = "hoodwink";
471+
Method m1 = c1.getDeclaredMethod(methodName1, String.class);
472+
473+
assertMethodDescriptionMatches(m1, "hoodwink a stranger");
474+
}
475+
}
476+
477+
@Test
478+
public void genericMethodsMatchOnExtendedClassAndImplementedInterface() throws Exception {
479+
try (CompilationClassLoader classLoader = compile(null)) {
480+
Class<?> c1 = classLoader.loadClass(VERY_COMPLEX_IMPLEMENTATION);
481+
482+
final String methodName3 = "fling";
483+
Method m2 = c1.getDeclaredMethod(methodName3, Integer.class);
484+
assertMethodDescriptionMatches(m2, "Fling the tea");
234485
}
235486
}
236487

@@ -275,6 +526,14 @@ private static void assertMethodMatches(Method method, String expectedDescriptio
275526
assertEquals(seeAlso4.getHtmlLink().getText(), "Moomoo land");
276527
}
277528

529+
private static void assertMethodDescriptionMatches(Method method, String expectedDescription) {
530+
MethodJavadoc methodDoc = expectJavadoc(method);
531+
assertEquals(method.getName(), methodDoc.getName());
532+
533+
String actualDesc = formatter.format(methodDoc.getComment());
534+
assertEquals(expectedDescription, actualDesc);
535+
}
536+
278537
@Test
279538
public void nestedClassNameIsPreserved() throws Exception {
280539
try (CompilationClassLoader classLoader = compile(null)) {
@@ -374,6 +633,13 @@ private static void expectNoJavadoc(Class<?> c) {
374633
assertEquals(c.getName(), doc.getName());
375634
}
376635

636+
private static void expectNoJavadoc(Method m) {
637+
MethodJavadoc doc = RuntimeJavadoc.getJavadoc(m);
638+
assertNotNull(doc);
639+
assertTrue(doc.isEmpty());
640+
assertEquals(m.getName(), doc.getName());
641+
}
642+
377643
private static <T extends BaseJavadoc> T assertPresent(T value, String msg) {
378644
if (value == null || value.isEmpty()) {
379645
throw new AssertionError(msg);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package javasource.bar;
2+
3+
import java.util.List;
4+
import javasource.foo.DocumentedClass;
5+
6+
7+
// I override methods of DocumentedClass with and without their own javadoc
8+
public class OverridingClassInAnotherPackage extends DocumentedClass<Object> {
9+
10+
/**
11+
* Quick frobulate {@code a} by {@code b} using thin frobulation
12+
*/
13+
public int frobulate(String a, int b) {
14+
throw new UnsupportedOperationException();
15+
}
16+
17+
// I have no javadoc of my own
18+
public int frobulate(String a, List<Integer> b) {
19+
throw new UnsupportedOperationException();
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package javasource.foo;
2+
3+
import javasource.foo.OtherInterface;
4+
import javasource.foo.DocumentedInterface;
5+
6+
public class ComplexImplementation implements DocumentedInterface<Integer>, OtherInterface<Integer> {
7+
// I have no javadoc of my own
8+
public boolean hoodwink(String i) {
9+
throw new UnsupportedOperationException();
10+
}
11+
12+
// I have no javadoc of my own
13+
public boolean snaggle(String i) {
14+
throw new UnsupportedOperationException();
15+
}
16+
17+
public boolean fling(Integer v) {
18+
throw new UnsupportedOperationException();
19+
}
20+
}

0 commit comments

Comments
 (0)