Skip to content

Commit

Permalink
Resolves #61. Inherit missing javadoc components from overridden meth…
Browse files Browse the repository at this point in the history
…ods (#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
  • Loading branch information
Sheikah45 authored Jun 10, 2022
1 parent daea64a commit 7e0e27d
Show file tree
Hide file tree
Showing 18 changed files with 1,029 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.github.therapi.runtimejavadoc.scribe.JavadocAnnotationProcessor;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
import org.junit.Test;

import javax.tools.JavaFileObject;
Expand All @@ -26,6 +28,14 @@ public class JavadocAnnotationProcessorTest {
private static final String DOCUMENTED_CLASS = "javasource.foo.DocumentedClass";
private static final String DOCUMENTED_ENUM = "javasource.foo.DocumentedEnum";
private static final String COMPLEX_ENUM = "javasource.foo.ComplexEnum";
private static final String OVERRIDING_CLASS_IN_ANOTHER_PACKAGE = "javasource.bar.OverridingClassInAnotherPackage";
private static final String OVERRIDING_CLASS = "javasource.foo.OverridingClass";
private static final String OVERRIDING_CLASS_2_DEGREES = "javasource.foo.OverridingClass2Degrees";
private static final String OTHER_INTERFACE = "javasource.foo.OtherInterface";
private static final String DOCUMENTED_INTERFACE = "javasource.foo.DocumentedInterface";
private static final String DOCUMENTED_IMPLEMENTATION = "javasource.foo.DocumentedImplementation";
private static final String COMPLEX_IMPLEMENTATION = "javasource.foo.ComplexImplementation";
private static final String VERY_COMPLEX_IMPLEMENTATION = "javasource.foo.VeryComplexImplementation";
private static final String ANOTHER_DOCUMENTED_CLASS = "javasource.bar.AnotherDocumentedClass";
private static final String ANNOTATED_WITH_RETAIN_JAVADOC = "javasource.bar.YetAnotherDocumentedClass";
private static final String UNDOCUMENTED = "javasource.bar.UndocumentedClass";
Expand All @@ -41,6 +51,14 @@ private static List<JavaFileObject> sources() {
"javasource/foo/DocumentedClass.java",
"javasource/foo/DocumentedEnum.java",
"javasource/foo/ComplexEnum.java",
"javasource/foo/OverridingClass.java",
"javasource/foo/OverridingClass2Degrees.java",
"javasource/foo/DocumentedInterface.java",
"javasource/foo/DocumentedImplementation.java",
"javasource/foo/ComplexImplementation.java",
"javasource/foo/VeryComplexImplementation.java",
"javasource/foo/OtherInterface.java",
"javasource/bar/OverridingClassInAnotherPackage.java",
"javasource/bar/AnotherDocumentedClass.java",
"javasource/bar/YetAnotherDocumentedClass.java",
"javasource/bar/UndocumentedClass.java",
Expand Down Expand Up @@ -80,6 +98,17 @@ public void classNameIsPreserved() throws Exception {
}
}

@Test
public void methodsFullyPopulatedByDefault() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);
ClassJavadoc classJavadoc = expectJavadoc(c);

Method m = c.getMethod("frobulate", String.class, List.class);
assertFalse(classJavadoc.findMatchingMethod(m).isEmpty());
}
}

@Test
public void retainFromAllPackages() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Expand Down Expand Up @@ -231,6 +260,228 @@ public void methodsMatchDespiteOverload() throws Exception {

assertMethodMatches(m1, "Frobulate <code>a</code> by <code>b</code>");
assertMethodMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");

Method m3 = c.getDeclaredMethod("equals", Object.class);

// javadoc tools do not inherit javadoc from Object
expectNoJavadoc(m3);
}
}

@Test
public void methodsMatchDespiteExtendingFromAnotherPackage() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS_IN_ANOTHER_PACKAGE);

final String methodName = "frobulate";
Method m1 = c.getDeclaredMethod(methodName, String.class, int.class);
Method m2 = c.getDeclaredMethod(methodName, String.class, List.class);

assertMethodDescriptionMatches(m1, "Quick frobulate <code>a</code> by <code>b</code> using thin frobulation");
assertMethodDescriptionMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");
}
}

@Test
public void methodsMatchWithExtendedClass() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);

final String methodName = "frobulate";
Method m1 = c.getDeclaredMethod(methodName, String.class, int.class);

MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
assertEquals(m1.getName(), methodJavadoc1.getName());

String actualDesc = formatter.format(methodJavadoc1.getComment());
assertEquals("Super frobulate <code>a</code> by <code>b</code> using extended frobulation", actualDesc);
assertEquals(2, methodJavadoc1.getParams().size());
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
assertFalse(methodJavadoc1.getThrows().isEmpty());

Method m2 = c.getDeclaredMethod(methodName, String.class, List.class);
assertMethodDescriptionMatches(m2, "Frobulate <code>a</code> by multiple oopsifizzle constants");
}
}

@Test
public void methodsMatchWithExtendedClass2Degrees() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS_2_DEGREES);

final String methodName = "skipMethod";
Method m1 = c.getDeclaredMethod(methodName);

MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
assertEquals(m1.getName(), methodJavadoc1.getName());

String actualDesc = formatter.format(methodJavadoc1.getComment());
assertEquals("I am also a simple method", actualDesc);
assertFalse(methodJavadoc1.getThrows().isEmpty());
}
}

@Test
public void genericMethodsMatchWithExtendedClass() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(OVERRIDING_CLASS);

final String methodName1 = "genericMethod";
Method m1 = c.getDeclaredMethod(methodName1, String.class);

assertMethodDescriptionMatches(m1, "Generic method to do generic things");

final String methodName2 = "separateGeneric";
Method m2 = c.getDeclaredMethod(methodName2, Integer.class);

expectNoJavadoc(m2);
}
}

@Test
public void genericMethodsMatchWithClass() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(DOCUMENTED_CLASS);

final String methodName1 = "genericMethod";
Method m1 = c.getDeclaredMethod(methodName1, Object.class);

assertMethodDescriptionMatches(m1, "Generic method to do generic things");

final String methodName2 = "separateGeneric";
Method m2 = c.getDeclaredMethod(methodName2, Comparable.class);

assertMethodDescriptionMatches(m2, "Generic method to do other things");
}
}

@Test
public void methodsMatchWithImplementation() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(DOCUMENTED_IMPLEMENTATION);

final String methodName = "hoodwink";
Method m1 = c.getDeclaredMethod(methodName, String.class);

MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
assertEquals(m1.getName(), methodJavadoc1.getName());

String actualDesc = formatter.format(methodJavadoc1.getComment());
assertEquals("hoodwink a stranger", actualDesc);
assertEquals(1, methodJavadoc1.getParams().size());
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
assertFalse(methodJavadoc1.getThrows().isEmpty());

final String methodName2 = "snaggle";
Method m2 = c.getDeclaredMethod(methodName2, String.class);
assertMethodDescriptionMatches(m2, "Snaggle a kerfluffin");
}
}

@Test
public void genericMethodsMatchWithImplementation() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c = classLoader.loadClass(DOCUMENTED_IMPLEMENTATION);

final String methodName1 = "fling";

Method m1 = c.getDeclaredMethod(methodName1, Integer.class);

MethodJavadoc methodJavadoc1 = expectJavadoc(m1);
assertEquals(m1.getName(), methodJavadoc1.getName());
String actualDesc = formatter.format(methodJavadoc1.getComment());
assertEquals("Fling the tea", actualDesc);
assertEquals(1, methodJavadoc1.getParams().size());
assertFalse(methodJavadoc1.getReturns().getElements().isEmpty());
assertFalse(methodJavadoc1.getThrows().isEmpty());
assertEquals(methodJavadoc1.getParamTypes(), Arrays.asList("java.lang.Integer"));
assertEquals("the tea weight", formatter.format(methodJavadoc1.getParams().get(0).getComment()));

Method m2 = c.getDeclaredMethod(methodName1, Object.class);
expectNoJavadoc(m2);
}
}

@Test
public void methodsMatchOnInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(DOCUMENTED_INTERFACE);
Class<?> c2 = classLoader.loadClass(OTHER_INTERFACE);

final String methodName1 = "hoodwink";
Method m1 = c1.getDeclaredMethod(methodName1, String.class);
Method m2 = c2.getDeclaredMethod(methodName1, String.class);

assertMethodDescriptionMatches(m1, "Hoodwink a kerfluffin");
assertMethodDescriptionMatches(m2, "Hoodwink a schmadragon");

final String methodName2 = "snaggle";
Method m3 = c1.getDeclaredMethod(methodName2, String.class);
assertMethodDescriptionMatches(m3, "Snaggle a kerfluffin");
}
}

@Test
public void genericMethodsMatchOnInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(DOCUMENTED_INTERFACE);
Class<?> c2 = classLoader.loadClass(OTHER_INTERFACE);

final String methodName3 = "fling";
Method m4 = c1.getDeclaredMethod(methodName3, Number.class);
Method m5 = c2.getDeclaredMethod(methodName3, Number.class);
assertMethodDescriptionMatches(m4, "Fling the tea");
assertMethodDescriptionMatches(m5, "Fling the vorrdin");
}
}

@Test
public void methodsMatchOnMultipleImplementedInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(COMPLEX_IMPLEMENTATION);

final String methodName1 = "hoodwink";
Method m1 = c1.getDeclaredMethod(methodName1, String.class);

assertMethodDescriptionMatches(m1, "Hoodwink a kerfluffin");

final String methodName2 = "snaggle";
Method m2 = c1.getDeclaredMethod(methodName2, String.class);
assertMethodDescriptionMatches(m2, "Snaggle a kerfluffin");
}
}

@Test
public void genericMethodsMatchOnMultipleImplementedInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(COMPLEX_IMPLEMENTATION);

final String methodName3 = "fling";
Method m3 = c1.getDeclaredMethod(methodName3, Integer.class);
assertMethodDescriptionMatches(m3, "Fling the tea");
}
}

@Test
public void methodsMatchOnExtendedClassAndImplementedInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(VERY_COMPLEX_IMPLEMENTATION);

final String methodName1 = "hoodwink";
Method m1 = c1.getDeclaredMethod(methodName1, String.class);

assertMethodDescriptionMatches(m1, "hoodwink a stranger");
}
}

@Test
public void genericMethodsMatchOnExtendedClassAndImplementedInterface() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Class<?> c1 = classLoader.loadClass(VERY_COMPLEX_IMPLEMENTATION);

final String methodName3 = "fling";
Method m2 = c1.getDeclaredMethod(methodName3, Integer.class);
assertMethodDescriptionMatches(m2, "Fling the tea");
}
}

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

private static void assertMethodDescriptionMatches(Method method, String expectedDescription) {
MethodJavadoc methodDoc = expectJavadoc(method);
assertEquals(method.getName(), methodDoc.getName());

String actualDesc = formatter.format(methodDoc.getComment());
assertEquals(expectedDescription, actualDesc);
}

@Test
public void nestedClassNameIsPreserved() throws Exception {
try (CompilationClassLoader classLoader = compile(null)) {
Expand Down Expand Up @@ -374,6 +633,13 @@ private static void expectNoJavadoc(Class<?> c) {
assertEquals(c.getName(), doc.getName());
}

private static void expectNoJavadoc(Method m) {
MethodJavadoc doc = RuntimeJavadoc.getJavadoc(m);
assertNotNull(doc);
assertTrue(doc.isEmpty());
assertEquals(m.getName(), doc.getName());
}

private static <T extends BaseJavadoc> T assertPresent(T value, String msg) {
if (value == null || value.isEmpty()) {
throw new AssertionError(msg);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package javasource.bar;

import java.util.List;
import javasource.foo.DocumentedClass;


// I override methods of DocumentedClass with and without their own javadoc
public class OverridingClassInAnotherPackage extends DocumentedClass<Object> {

/**
* Quick frobulate {@code a} by {@code b} using thin frobulation
*/
public int frobulate(String a, int b) {
throw new UnsupportedOperationException();
}

// I have no javadoc of my own
public int frobulate(String a, List<Integer> b) {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package javasource.foo;

import javasource.foo.OtherInterface;
import javasource.foo.DocumentedInterface;

public class ComplexImplementation implements DocumentedInterface<Integer>, OtherInterface<Integer> {
// I have no javadoc of my own
public boolean hoodwink(String i) {
throw new UnsupportedOperationException();
}

// I have no javadoc of my own
public boolean snaggle(String i) {
throw new UnsupportedOperationException();
}

public boolean fling(Integer v) {
throw new UnsupportedOperationException();
}
}
Loading

0 comments on commit 7e0e27d

Please sign in to comment.