From 7e0e27d90cf7a56263512121344511996bb6646c Mon Sep 17 00:00:00 2001 From: Sheikah45 <66929319+Sheikah45@users.noreply.github.com> Date: Fri, 10 Jun 2022 14:47:30 -0400 Subject: [PATCH] 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 --- .../JavadocAnnotationProcessorTest.java | 266 ++++++++++++++++++ .../bar/OverridingClassInAnotherPackage.java | 21 ++ .../javasource/foo/ComplexImplementation.java | 20 ++ .../javasource/foo/DocumentedClass.java | 45 ++- .../foo/DocumentedImplementation.java | 28 ++ .../javasource/foo/DocumentedInterface.java | 32 +++ .../javasource/foo/OtherInterface.java | 19 ++ .../javasource/foo/OverridingClass.java | 44 +++ .../foo/OverridingClass2Degrees.java | 20 ++ .../foo/VeryComplexImplementation.java | 15 + .../therapi/runtimejavadoc/ClassJavadoc.java | 154 ++++++++-- .../therapi/runtimejavadoc/Comment.java | 2 +- .../therapi/runtimejavadoc/MethodJavadoc.java | 141 ++++++++-- .../runtimejavadoc/RuntimeJavadoc.java | 113 ++++---- .../internal/MethodJavadocKey.java | 40 +++ .../internal/RuntimeJavadocHelper.java | 84 +++++- .../internal/parser/JavadocParser.java | 115 +++----- .../internal/parser/ParsedJavadoc.java | 53 +++- 18 files changed, 1029 insertions(+), 183 deletions(-) create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/bar/OverridingClassInAnotherPackage.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/ComplexImplementation.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedImplementation.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedInterface.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OtherInterface.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass2Degrees.java create mode 100644 therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/VeryComplexImplementation.java create mode 100644 therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/MethodJavadocKey.java diff --git a/therapi-runtime-javadoc-scribe/src/test/java/com/github/therapi/runtimejavadoc/JavadocAnnotationProcessorTest.java b/therapi-runtime-javadoc-scribe/src/test/java/com/github/therapi/runtimejavadoc/JavadocAnnotationProcessorTest.java index f677fc0..4400098 100644 --- a/therapi-runtime-javadoc-scribe/src/test/java/com/github/therapi/runtimejavadoc/JavadocAnnotationProcessorTest.java +++ b/therapi-runtime-javadoc-scribe/src/test/java/com/github/therapi/runtimejavadoc/JavadocAnnotationProcessorTest.java @@ -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; @@ -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"; @@ -41,6 +51,14 @@ private static List 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", @@ -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)) { @@ -231,6 +260,228 @@ public void methodsMatchDespiteOverload() throws Exception { assertMethodMatches(m1, "Frobulate a by b"); assertMethodMatches(m2, "Frobulate a 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 a by b using thin frobulation"); + assertMethodDescriptionMatches(m2, "Frobulate a 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 a by b 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 a 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"); } } @@ -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)) { @@ -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 assertPresent(T value, String msg) { if (value == null || value.isEmpty()) { throw new AssertionError(msg); diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/bar/OverridingClassInAnotherPackage.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/bar/OverridingClassInAnotherPackage.java new file mode 100644 index 0000000..abbc5a9 --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/bar/OverridingClassInAnotherPackage.java @@ -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 { + + /** + * 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 b) { + throw new UnsupportedOperationException(); + } +} diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/ComplexImplementation.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/ComplexImplementation.java new file mode 100644 index 0000000..d2f4120 --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/ComplexImplementation.java @@ -0,0 +1,20 @@ +package javasource.foo; + +import javasource.foo.OtherInterface; +import javasource.foo.DocumentedInterface; + +public class ComplexImplementation implements DocumentedInterface, OtherInterface { + // 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(); + } +} \ No newline at end of file diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedClass.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedClass.java index f99a2bf..ba2dd0a 100644 --- a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedClass.java +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedClass.java @@ -9,7 +9,7 @@ * @custom.tag What does {@custom.inline this} mean? */ -public class DocumentedClass { +public class DocumentedClass { /** * I'm a useful field, maybe. @@ -18,6 +18,11 @@ public class DocumentedClass { */ private int myField; + /** + * I'm a field my children can see. + */ + protected int ourField; + /** * I'm a constructor! */ @@ -72,6 +77,15 @@ public void someOtherMethod() { } + /** + * I am a simple method + * + * @throws UnsupportedOperationException if cannot skip + */ + public void skipMethod() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + /** * Foo {@link Foo#bar(String).}{@value Foo#bar(String).} * @@ -80,6 +94,35 @@ public void someOtherMethod() { public void malformedLinks() { } + /** + * Generic method to do generic things + */ + public T genericMethod(T generic) { + return generic; + } + + /** + * Generic method to do run things + */ + public T skipGenericMethod(T generic) { + return generic; + } + + /** + * Generic method to do other things + */ + public > T separateGeneric(U otherGeneric) { + throw new UnsupportedOperationException(); + } + + public T blankGenericMethod() { + throw new UnsupportedOperationException(); + } + + public boolean equals(Object o) { + return super.equals(o); + } + /** * I'm a nested class! */ diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedImplementation.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedImplementation.java new file mode 100644 index 0000000..34fc9a9 --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedImplementation.java @@ -0,0 +1,28 @@ +package javasource.foo; + +import javasource.foo.DocumentedInterface; + +public class DocumentedImplementation implements DocumentedInterface { + /** + * hoodwink a stranger + */ + public boolean hoodwink(String i) { + throw new UnsupportedOperationException(); + } + + // I have no javadoc of my own + public boolean snaggle(String i) { + throw new UnsupportedOperationException(); + } + + /** + * @param v the tea weight + */ + public boolean fling(Integer v) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + public boolean fling(Object v) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedInterface.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedInterface.java new file mode 100644 index 0000000..68f365e --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/DocumentedInterface.java @@ -0,0 +1,32 @@ +package javasource.foo; + +/** + * The {@code Javadoc} from this interface is used for testing + * + */ +public interface DocumentedInterface { + /** + * Hoodwink a kerfluffin + * + * @param i innocent + * @return true if innocent hoodwinked + * @throws UnsupportedOperationException if hoodwinking cannot be performed + */ + public boolean hoodwink(String i) throws UnsupportedOperationException; + + /** + * Snaggle a kerfluffin + * + * @param i innocent + * @return true if innocent hoodwinked + */ + public boolean snaggle(String i); + + /** + * Fling the tea + * @param v + * @return true if flung + * @exception UnsupportedOperationException if hoodwinking cannot be performed + */ + public boolean fling(T v) throws UnsupportedOperationException; +} \ No newline at end of file diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OtherInterface.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OtherInterface.java new file mode 100644 index 0000000..6042146 --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OtherInterface.java @@ -0,0 +1,19 @@ +package javasource.foo; + +/** + * The {@code Javadoc} from this interface is used for masking + * + */ +public interface OtherInterface { + /** + * Hoodwink a schmadragon + */ + public boolean hoodwink(String g); + + /** + * Fling the vorrdin + * @param v + * @return + */ + public boolean fling(T v); +} \ No newline at end of file diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass.java new file mode 100644 index 0000000..76d688c --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass.java @@ -0,0 +1,44 @@ +package javasource.foo; + +import java.util.List; +import javasource.foo.DocumentedClass; + + +// I override methods of DocumentedClass with and without their own javadoc +public class OverridingClass extends DocumentedClass { + + /** + * Super frobulate {@code a} by {@code b} using extended frobulation + * + * @see com.github.therapi.runtimejavadoc.DocumentedClass Hey, that's this class! + * @see javasource.foo.DocumentedClass#someOtherMethod() + * @see "Moomoo boy went straight to Moomoo land. Land of the moomoo's" + * @see Moomoo land + */ + public int frobulate(String a, int b) { + throw new UnsupportedOperationException(); + } + + // I have no javadoc of my own + public int frobulate(String a, List b) { + throw new UnsupportedOperationException(); + } + + /** + * My very own method + * + */ + public void myOwnMethod() { + throw new UnsupportedOperationException(); + } + + // I have no javadoc + public String genericMethod(String generic) { + return generic; + } + + // Even though I may no look like it I override nothing but do partially hide a method + public String separateGeneric(Integer otherGeneric) { + throw new UnsupportedOperationException(); + } +} diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass2Degrees.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass2Degrees.java new file mode 100644 index 0000000..7d6c08a --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/OverridingClass2Degrees.java @@ -0,0 +1,20 @@ +package javasource.foo; + +import java.util.List; +import javasource.foo.OverridingClass; + + +// I override methods of DocumentedClass with and without their own javadoc +public class OverridingClass2Degrees extends OverridingClass { + + /** + * I am also a simple method + */ + public void skipMethod() { + throw new UnsupportedOperationException(); + } + + public String skipGenericMethod(String generic) { + return generic; + } +} diff --git a/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/VeryComplexImplementation.java b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/VeryComplexImplementation.java new file mode 100644 index 0000000..73c45d0 --- /dev/null +++ b/therapi-runtime-javadoc-scribe/src/test/resources/javasource/foo/VeryComplexImplementation.java @@ -0,0 +1,15 @@ +package javasource.foo; + +import javasource.foo.OtherInterface; +import javasource.foo.DocumentedInterface; + +public class VeryComplexImplementation extends DocumentedImplementation implements OtherInterface { + // I have no javadoc of my own + public boolean hoodwink(String i) { + throw new UnsupportedOperationException(); + } + + public boolean fling(Integer v) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/ClassJavadoc.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/ClassJavadoc.java index 4279be2..33c6bcb 100755 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/ClassJavadoc.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/ClassJavadoc.java @@ -16,31 +16,79 @@ package com.github.therapi.runtimejavadoc; +import com.github.therapi.runtimejavadoc.internal.MethodJavadocKey; +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.executableToMethodJavadocKey; +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.getAllTypeAncestors; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; - -import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.unmodifiableDefensiveCopy; +import java.util.Map; public class ClassJavadoc extends BaseJavadoc { - - private final List fields; - private final List enumConstants; - private final List methods; - private final List constructors; - private final List recordComponents; + private final Map fields; + private final Map enumConstants; + private final Map methods; + private final Map constructors; + private final Map recordComponents; public ClassJavadoc(String name, Comment comment, List fields, List enumConstants, - List methods, List constructors, List other, List seeAlso, - List recordComponents) { + List methods, List constructors, List other, + List seeAlso, List recordComponents) { + super(name, comment, seeAlso, other); + + Map fieldMap = new LinkedHashMap<>(); + if (fields != null) { + fields.forEach(fieldJavadoc -> fieldMap.put(fieldJavadoc.getName(), fieldJavadoc)); + } + this.fields = Collections.unmodifiableMap(fieldMap); + + Map enumMap = new LinkedHashMap<>(); + if (enumConstants != null) { + enumConstants.forEach(fieldJavadoc -> enumMap.put(fieldJavadoc.getName(), fieldJavadoc)); + } + this.enumConstants = Collections.unmodifiableMap(enumMap); + + Map methodsMap = new LinkedHashMap<>(); + if (methods != null) { + methods.forEach(methodJavadoc -> methodsMap.put(methodJavadoc.toMethodJavadocKey(), methodJavadoc)); + } + this.methods = Collections.unmodifiableMap(methodsMap); + + Map constructorsMap = new LinkedHashMap<>(); + if (constructors != null) { + constructors.forEach( + methodJavadoc -> constructorsMap.put(methodJavadoc.toMethodJavadocKey(), methodJavadoc)); + } + this.constructors = Collections.unmodifiableMap(constructorsMap); + + Map recordsMap = new LinkedHashMap<>(); + if (recordComponents != null) { + recordComponents.forEach(paramJavadoc -> recordsMap.put(paramJavadoc.getName(), paramJavadoc)); + } + this.recordComponents = Collections.unmodifiableMap(recordsMap); + } + + private ClassJavadoc(String name, Comment comment, Map fields, + Map enumConstants, Map methods, + Map constructors, List other, + List seeAlso, Map recordComponents) { super(name, comment, seeAlso, other); - this.fields = unmodifiableDefensiveCopy(fields); - this.enumConstants = unmodifiableDefensiveCopy(enumConstants); - this.methods = unmodifiableDefensiveCopy(methods); - this.constructors = unmodifiableDefensiveCopy(constructors); - this.recordComponents = unmodifiableDefensiveCopy(recordComponents); + this.fields = Collections.unmodifiableMap(fields); + this.enumConstants = Collections.unmodifiableMap(enumConstants); + this.methods = Collections.unmodifiableMap(methods); + this.constructors = Collections.unmodifiableMap(constructors); + this.recordComponents = Collections.unmodifiableMap(recordComponents); } public static ClassJavadoc createEmpty(String qualifiedClassName) { - return new ClassJavadoc(qualifiedClassName, null, null, null, null, null, null, null, null) { + return new ClassJavadoc(qualifiedClassName, null, (List) null, null, null, null, null, null, + null) { @Override public boolean isEmpty() { return true; @@ -48,20 +96,43 @@ public boolean isEmpty() { }; } + ClassJavadoc createEnhancedClassJavadoc(Class clazz) { + if (!getName().equals(clazz.getCanonicalName())) { + throw new IllegalArgumentException("Class `" + clazz.getCanonicalName() + "` does not match class doc for `" + getName() + "`"); + } + + if (isEmpty()) { + return this; + } + + Map classJavadocCache = new HashMap<>(); + + classJavadocCache.put(clazz.getCanonicalName(), this); + getAllTypeAncestors(clazz).forEach(cls -> classJavadocCache.put(cls.getCanonicalName(), RuntimeJavadoc.getSkinnyClassJavadoc(cls))); + + Map methodJavadocs = new LinkedHashMap<>(); + Arrays.stream(clazz.getDeclaredMethods()) + .forEach(method -> methodJavadocs.put(executableToMethodJavadocKey(method), + RuntimeJavadoc.getJavadoc(method, classJavadocCache))); + + return new ClassJavadoc(getName(), getComment(), fields, enumConstants, methodJavadocs, constructors, + getOther(), getSeeAlso(), recordComponents); + } + public List getFields() { - return fields; + return Collections.unmodifiableList(new ArrayList<>(fields.values())); } public List getEnumConstants() { - return enumConstants; + return Collections.unmodifiableList(new ArrayList<>(enumConstants.values())); } public List getMethods() { - return methods; + return Collections.unmodifiableList(new ArrayList<>(methods.values())); } public List getConstructors() { - return constructors; + return Collections.unmodifiableList(new ArrayList<>(constructors.values())); } /** @@ -72,20 +143,41 @@ public List getConstructors() { * in the order the tags appear in the Javadoc. */ public List getRecordComponents() { - return recordComponents; + return Collections.unmodifiableList(new ArrayList<>(recordComponents.values())); + } + + FieldJavadoc findMatchingField(Field field) { + return fields.getOrDefault(field.getName(), FieldJavadoc.createEmpty(field.getName())); + } + + FieldJavadoc findMatchingEnumConstant(Enum enumConstant) { + return enumConstants.getOrDefault(enumConstant.name(), FieldJavadoc.createEmpty(enumConstant.name())); + } + + MethodJavadoc findMatchingMethod(Method method) { + MethodJavadocKey methodJavadocKey = executableToMethodJavadocKey(method); + return methods.getOrDefault(methodJavadocKey, MethodJavadoc.createEmpty(method)); + } + + MethodJavadoc findMatchingConstructor(Constructor constructor) { + MethodJavadocKey methodJavadocKey = executableToMethodJavadocKey(constructor); + return constructors.getOrDefault(methodJavadocKey, MethodJavadoc.createEmpty(constructor)); + } + + ParamJavadoc findRecordComponent(String recordComponent) { + return recordComponents.getOrDefault(recordComponent, new ParamJavadoc(recordComponent, Comment.createEmpty())); } @Override public String toString() { - return "ClassJavadoc{" + - "name='" + getName() + '\'' + - ", comment=" + getComment() + - ", fields=" + fields + - ", methods=" + methods + - ", constructors=" + constructors + - ", recordComponents=" + recordComponents + - ", seeAlso=" + getSeeAlso() + - ", other=" + getOther() + - '}'; + return "ClassJavadoc{" + + "name='" + getName() + '\'' + + ", comment=" + getComment() + + ", fields=" + fields + + ", methods=" + methods + + ", constructors=" + constructors + + ", recordComponents=" + recordComponents + + ", seeAlso=" + getSeeAlso() + + ", other=" + getOther() + '}'; } } diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/Comment.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/Comment.java index c65c228..0d1070f 100755 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/Comment.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/Comment.java @@ -26,7 +26,7 @@ * Comment text that may contain inline tags. */ public class Comment implements Iterable { - private static final Comment EMPTY = new Comment(Collections.emptyList()); + private static final Comment EMPTY = new Comment(Collections.emptyList()); public static Comment createEmpty() { return EMPTY; diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/MethodJavadoc.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/MethodJavadoc.java index c144749..55940e8 100755 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/MethodJavadoc.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/MethodJavadoc.java @@ -16,17 +16,24 @@ package com.github.therapi.runtimejavadoc; +import com.github.therapi.runtimejavadoc.internal.MethodJavadocKey; +import com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper; +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.unmodifiableDefensiveCopy; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; - -import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.unmodifiableDefensiveCopy; +import java.util.Map; +import java.util.stream.Collectors; public class MethodJavadoc extends BaseJavadoc { private final List paramTypes; - private final List params; - private final List exceptions; + private final Map params; + private final Map exceptions; private final Comment returns; public MethodJavadoc(String name, @@ -38,14 +45,49 @@ public MethodJavadoc(String name, Comment returns, List seeAlso) { super(name, comment, seeAlso, other); + this.paramTypes = unmodifiableDefensiveCopy(paramTypes); - this.params = unmodifiableDefensiveCopy(params); - this.exceptions = unmodifiableDefensiveCopy(exceptions); this.returns = Comment.nullToEmpty(returns); + + Map paramJavadocMap = new LinkedHashMap<>(); + + if (params != null) { + params.forEach(paramJavadoc -> paramJavadocMap.put(paramJavadoc.getName(), paramJavadoc)); + } + + this.params = Collections.unmodifiableMap(paramJavadocMap); + + Map throwsJavadocMap = new LinkedHashMap<>(); + + if (params != null) { + exceptions.forEach(throwsJavadoc -> throwsJavadocMap.put(throwsJavadoc.getName(), throwsJavadoc)); + } + + this.exceptions = Collections.unmodifiableMap(throwsJavadocMap); } - public static MethodJavadoc createEmpty(Method method) { - return new MethodJavadoc(method.getName(), null, null, null, null, null, null, null) { + private MethodJavadoc(String name, + List paramTypes, + Comment comment, + Map params, + Map exceptions, + List other, + Comment returns, + List seeAlso) { + super(name, comment, seeAlso, other); + this.paramTypes = Collections.unmodifiableList(paramTypes); + this.params = Collections.unmodifiableMap(params); + this.exceptions = Collections.unmodifiableMap(exceptions); + this.returns = returns; + } + + public static MethodJavadoc createEmpty(Executable executable) { + String name = executable instanceof Constructor ? RuntimeJavadocHelper.INIT : executable.getName(); + List paramTypes = Arrays.stream(executable.getParameterTypes()) + .map(Class::getCanonicalName) + .collect(Collectors.toList()); + + return new MethodJavadoc(name, paramTypes, null, (List) null, null, null, null, null) { @Override public boolean isEmpty() { return true; @@ -53,17 +95,78 @@ public boolean isEmpty() { }; } - public static MethodJavadoc createEmpty(Constructor method) { - return new MethodJavadoc("", null, null, null, null, null, null, null) { - @Override - public boolean isEmpty() { - return true; + MethodJavadoc enhanceWithOverriddenJavadoc(Method method, Map classJavadocCache) { + MethodJavadoc enhancedJavadoc = this; + List> superTypes = RuntimeJavadocHelper.getAllTypeAncestors(method.getDeclaringClass()); + + for (Class superType : superTypes) { + ClassJavadoc classJavadoc = classJavadocCache.get(superType.getCanonicalName()); + if (classJavadoc == null) { + classJavadoc = RuntimeJavadoc.getSkinnyClassJavadoc(superType); } - }; + MethodJavadoc overriddenJavadoc = classJavadoc.findMatchingMethod(method); + enhancedJavadoc = enhancedJavadoc.copyWithInheritance(overriddenJavadoc); + if (enhancedJavadoc.fullyDescribes(method)) { + break; + } + } + + return enhancedJavadoc; + } + + private MethodJavadoc copyWithInheritance(MethodJavadoc superMethodJavadoc) { + if (superMethodJavadoc.isEmpty()) { + return this; + } + + List paramTypes = new ArrayList<>(this.paramTypes); + if (paramTypes.isEmpty()) { + paramTypes = superMethodJavadoc.paramTypes; + } + + Comment comment = getComment(); + if (comment.getElements().isEmpty()) { + comment = superMethodJavadoc.getComment(); + } + + Map params = new LinkedHashMap<>(this.params); + superMethodJavadoc.params.forEach(params::putIfAbsent); + + Map exceptions = new LinkedHashMap<>(this.exceptions); + superMethodJavadoc.exceptions.forEach(exceptions::putIfAbsent); + + Comment returns = this.returns; + if (returns.getElements().isEmpty()) { + returns = superMethodJavadoc.returns; + } + + return new MethodJavadoc(getName(), paramTypes, comment, params, exceptions, getOther(), returns, getSeeAlso()); } public boolean isConstructor() { - return "".equals(getName()); + return RuntimeJavadocHelper.INIT.equals(getName()); + } + + boolean fullyDescribes(Method method) { + if (!method.getName().equals(getName()) || method.getParameterCount() != paramTypes.size()) { + throw new IllegalArgumentException("Method `" + method.getName() + "` does not match javadoc `" + getName() + "`"); + } + + return !getComment().getElements().isEmpty() + && !returns.getElements().isEmpty() + && method.getParameterCount() == params.size() + && Arrays.stream(method.getExceptionTypes()) + .allMatch(exception -> exceptions.containsKey(exception.getSimpleName())); + } + + public boolean matches(Executable executable) { + if (executable instanceof Method) { + return matches((Method) executable); + } else if (executable instanceof Constructor) { + return matches((Constructor) executable); + } else { + throw new UnsupportedOperationException("Unknown executable type"); + } } public boolean matches(Method method) { @@ -88,16 +191,20 @@ private static List getCanonicalNames(Class[] paramTypes) { return methodParamsTypes; } + MethodJavadocKey toMethodJavadocKey() { + return new MethodJavadocKey(getName(), paramTypes); + } + public List getParamTypes() { return paramTypes; } public List getParams() { - return params; + return Collections.unmodifiableList(new ArrayList<>(params.values())); } public List getThrows() { - return exceptions; + return Collections.unmodifiableList(new ArrayList<>(exceptions.values())); } public Comment getReturns() { diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/RuntimeJavadoc.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/RuntimeJavadoc.java index 4559edc..8b6ba26 100644 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/RuntimeJavadoc.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/RuntimeJavadoc.java @@ -19,17 +19,18 @@ import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonObject; import com.github.therapi.runtimejavadoc.internal.JsonJavadocReader; - +import com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper; +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.javadocResourceSuffix; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.List; - -import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.javadocResourceSuffix; import static java.nio.charset.StandardCharsets.UTF_8; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * Allows access to Javadoc elements at runtime for code that was compiled using the @@ -50,8 +51,8 @@ private RuntimeJavadoc() { * @param clazz the class whose Javadoc you want to retrieve * @return the Javadoc of the given class */ - public static ClassJavadoc getJavadoc(Class clazz) { - return getJavadoc(clazz.getName(), clazz); + public static ClassJavadoc getJavadoc(Class clazz) { + return getSkinnyClassJavadoc(clazz.getName(), clazz).createEnhancedClassJavadoc(clazz); } /** @@ -74,14 +75,14 @@ public static ClassJavadoc getJavadoc(String qualifiedClassName) { * {@link BaseJavadoc#isEmpty isEmpty()} method will return {@code true}. * * @param qualifiedClassName the fully qualified name of the class whose Javadoc you want to retrieve - * @param classLoader the class loader to use to find the Javadoc resource file + * @param loader the class loader to use to find the Javadoc resource file * @return the Javadoc of the given class */ - public static ClassJavadoc getJavadoc(String qualifiedClassName, ClassLoader classLoader) { - final String resourceName = getResourceName(qualifiedClassName); - try (InputStream is = classLoader.getResourceAsStream(resourceName)) { - return parseJavadocResource(qualifiedClassName, is); - } catch (IOException e) { + public static ClassJavadoc getJavadoc(String qualifiedClassName, ClassLoader loader) { + try { + return getSkinnyClassJavadoc(qualifiedClassName, loader) + .createEnhancedClassJavadoc(loader.loadClass(qualifiedClassName)); + } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } @@ -96,7 +97,29 @@ public static ClassJavadoc getJavadoc(String qualifiedClassName, ClassLoader cla * @param loader the class object to use to find the Javadoc resource file * @return the Javadoc of the given class */ - public static ClassJavadoc getJavadoc(String qualifiedClassName, Class loader) { + public static ClassJavadoc getJavadoc(String qualifiedClassName, Class loader) { + try { + return getSkinnyClassJavadoc(qualifiedClassName, loader) + .createEnhancedClassJavadoc(loader.getClassLoader().loadClass(qualifiedClassName)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + static ClassJavadoc getSkinnyClassJavadoc(Class clazz) { + return getSkinnyClassJavadoc(clazz.getName(), clazz); + } + + private static ClassJavadoc getSkinnyClassJavadoc(String qualifiedClassName, ClassLoader loader) { + final String resourceName = getResourceName(qualifiedClassName); + try (InputStream is = loader.getResourceAsStream("/" + resourceName)) { + return parseJavadocResource(qualifiedClassName, is); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static ClassJavadoc getSkinnyClassJavadoc(String qualifiedClassName, Class loader) { final String resourceName = getResourceName(qualifiedClassName); try (InputStream is = loader.getResourceAsStream("/" + resourceName)) { return parseJavadocResource(qualifiedClassName, is); @@ -135,8 +158,32 @@ private static ClassJavadoc parseJavadocResource(String qualifiedClassName, Inpu * @return the given method's Javadoc */ public static MethodJavadoc getJavadoc(Method method) { - ClassJavadoc javadoc = getJavadoc(method.getDeclaringClass()); - return findMethodJavadoc(javadoc.getMethods(), method); + return getJavadoc(method, Collections.emptyMap()); + } + + static MethodJavadoc getJavadoc(Method method, Map classJavadocCache) { + Class declaringClass = method.getDeclaringClass(); + ClassJavadoc classJavadoc = classJavadocCache.get(declaringClass.getCanonicalName()); + if (classJavadoc == null) { + classJavadoc = getSkinnyClassJavadoc(declaringClass); + } + + MethodJavadoc methodJavadoc = classJavadoc.findMatchingMethod(method); + if (methodJavadoc.fullyDescribes(method)) { + return methodJavadoc; + } + + methodJavadoc = methodJavadoc.enhanceWithOverriddenJavadoc(method, classJavadocCache); + if (methodJavadoc.fullyDescribes(method)) { + return methodJavadoc; + } + + Method bridgeMethod = RuntimeJavadocHelper.findBridgeMethod(method); + if (bridgeMethod != null && method != bridgeMethod) { + methodJavadoc = methodJavadoc.enhanceWithOverriddenJavadoc(bridgeMethod, classJavadocCache); + } + + return methodJavadoc; } /** @@ -154,26 +201,7 @@ public static MethodJavadoc getJavadoc(Method method) { * @return the given constructor's Javadoc */ public static MethodJavadoc getJavadoc(Constructor method) { - ClassJavadoc javadoc = getJavadoc(method.getDeclaringClass()); - return findMethodJavadoc(javadoc.getConstructors(), method); - } - - private static MethodJavadoc findMethodJavadoc(List methodDocs, Method method) { - for (MethodJavadoc methodJavadoc : methodDocs) { - if (methodJavadoc.matches(method)) { - return methodJavadoc; - } - } - return MethodJavadoc.createEmpty(method); - } - - private static MethodJavadoc findMethodJavadoc(List methodDocs, Constructor method) { - for (MethodJavadoc methodJavadoc : methodDocs) { - if (methodJavadoc.matches(method)) { - return methodJavadoc; - } - } - return MethodJavadoc.createEmpty(method); + return getSkinnyClassJavadoc(method.getDeclaringClass()).findMatchingConstructor(method); } /** @@ -191,8 +219,7 @@ private static MethodJavadoc findMethodJavadoc(List methodDocs, C * @return the given field's Javadoc */ public static FieldJavadoc getJavadoc(Field field) { - ClassJavadoc javadoc = getJavadoc(field.getDeclaringClass()); - return findFieldJavadoc(javadoc.getFields(), field.getName()); + return getSkinnyClassJavadoc(field.getDeclaringClass()).findMatchingField(field); } /** @@ -210,16 +237,6 @@ public static FieldJavadoc getJavadoc(Field field) { * @return the given enum constant's Javadoc */ public static FieldJavadoc getJavadoc(Enum enumValue) { - ClassJavadoc javadoc = getJavadoc(enumValue.getDeclaringClass()); - return findFieldJavadoc(javadoc.getEnumConstants(), enumValue.name()); - } - - private static FieldJavadoc findFieldJavadoc(List fieldDocs, String fieldName) { - for (FieldJavadoc fDoc : fieldDocs) { - if (fDoc.getName().equals(fieldName)) { - return fDoc; - } - } - return FieldJavadoc.createEmpty(fieldName); + return getSkinnyClassJavadoc(enumValue.getDeclaringClass()).findMatchingEnumConstant(enumValue); } } diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/MethodJavadocKey.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/MethodJavadocKey.java new file mode 100644 index 0000000..d38799f --- /dev/null +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/MethodJavadocKey.java @@ -0,0 +1,40 @@ +package com.github.therapi.runtimejavadoc.internal; + +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.unmodifiableDefensiveCopy; +import java.util.List; +import java.util.Objects; + +public class MethodJavadocKey { + private final String methodName; + private final List methodParamTypes; + + public MethodJavadocKey(String methodName, List methodParamTypes) { + this.methodName = methodName; + this.methodParamTypes = unmodifiableDefensiveCopy(methodParamTypes); + } + + public String getMethodName() { + return methodName; + } + + public List getMethodParamTypes() { + return methodParamTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MethodJavadocKey that = (MethodJavadocKey) o; + return Objects.equals(methodName, that.methodName) && Objects.equals(methodParamTypes, that.methodParamTypes); + } + + @Override + public int hashCode() { + return Objects.hash(methodName, methodParamTypes); + } +} diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/RuntimeJavadocHelper.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/RuntimeJavadocHelper.java index d6276ff..4547fe8 100644 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/RuntimeJavadocHelper.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/RuntimeJavadocHelper.java @@ -16,20 +16,28 @@ package com.github.therapi.runtimejavadoc.internal; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import static java.util.Collections.unmodifiableList; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; - -import static java.util.Collections.unmodifiableList; +import java.util.Map; +import java.util.stream.Collectors; public class RuntimeJavadocHelper { + public static final String INIT = ""; + private RuntimeJavadocHelper() { throw new AssertionError("not instantiable"); } public static List unmodifiableDefensiveCopy(List list) { - return list == null ? Collections.emptyList() : unmodifiableList(new ArrayList<>(list)); + return list == null ? Collections.emptyList() : unmodifiableList(new ArrayList<>(list)); } public static T requireNonNull(T object) { @@ -42,7 +50,7 @@ public static T requireNonNull(T object) { public static String join(CharSequence delimiter, Iterable items) { requireNonNull(delimiter); StringBuilder result = new StringBuilder(); - for (Iterator i = items.iterator(); i.hasNext();) { + for (Iterator i = items.iterator(); i.hasNext(); ) { result.append(i.next()); if (i.hasNext()) { result.append(delimiter); @@ -51,6 +59,43 @@ public static String join(CharSequence delimiter, Iterable paramTypes = Arrays.stream(executable.getParameterTypes()) + .map(Class::getCanonicalName) + .collect(Collectors.toList()); + String name; + if (executable instanceof Method) { + name = executable.getName(); + } else if (executable instanceof Constructor) { + name = INIT; + } else { + throw new UnsupportedOperationException("Unknown executable type"); + } + + return new MethodJavadocKey(name, paramTypes); + } + + public static List> getAllTypeAncestors(Class clazz) { + if (clazz == null) { + return Collections.emptyList(); + } + + Map> typeAncestors = new LinkedHashMap<>(); + Class superclass = clazz.getSuperclass(); + if (superclass != null) { + typeAncestors.put(superclass.getCanonicalName(), superclass); + getAllTypeAncestors(superclass).forEach(cls -> typeAncestors.put(cls.getCanonicalName(), cls)); + } + + Class[] interfaces = clazz.getInterfaces(); + for (Class superType : interfaces) { + typeAncestors.put(superType.getCanonicalName(), superType); + getAllTypeAncestors(superType).forEach(cls -> typeAncestors.put(cls.getCanonicalName(), cls)); + } + + return Collections.unmodifiableList(new ArrayList<>(typeAncestors.values())); + } + public static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } @@ -86,4 +131,35 @@ public static String elementNameFieldName() { public static String elementDocFieldName() { return "doc"; } + + public static Method findBridgeMethod(Method method) { + if (method.isBridge()) { + return method; + } + + Class declaringClass = method.getDeclaringClass(); + for (Method bridgeMethod : declaringClass.getDeclaredMethods()) { + if (bridgeMethod.isBridge() + && method.getName().equals(bridgeMethod.getName()) + && parametersMatchWithErasure(method.getParameterTypes(), bridgeMethod.getParameterTypes())) { + return bridgeMethod; + } + } + + return null; + } + + private static boolean parametersMatchWithErasure(Class[] parameterTypes, Class[] erasureTypes) { + if (parameterTypes.length != erasureTypes.length) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!erasureTypes[i].isAssignableFrom(parameterTypes[i])) { + return false; + } + } + + return true; + } } diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/JavadocParser.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/JavadocParser.java index 53f5cf8..6d64d50 100644 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/JavadocParser.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/JavadocParser.java @@ -24,7 +24,6 @@ import com.github.therapi.runtimejavadoc.ParamJavadoc; import com.github.therapi.runtimejavadoc.SeeAlsoJavadoc; import com.github.therapi.runtimejavadoc.ThrowsJavadoc; - import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -45,92 +44,60 @@ private static ParamJavadoc parseParam(BlockTag t, String owningClass) { public static ClassJavadoc parseClassJavadoc(String className, String javadoc, List fields, List enumConstants, List methods, List constructors) { - ParsedJavadoc parsed = parse(javadoc); - - List otherDocs = new ArrayList<>(); - List seeAlsoDocs = new ArrayList<>(); - List paramDocs = new ArrayList<>(); - - for (BlockTag t : parsed.getBlockTags()) { - if (t.name.equals("param")) { - paramDocs.add(parseParam(t, className)); - } else if (t.name.equals("see")) { - SeeAlsoJavadoc seeAlso = SeeAlsoParser.parseSeeAlso(className, t.value); - if (seeAlso != null) { - seeAlsoDocs.add(seeAlso); - } - } else { - otherDocs.add(new OtherJavadoc(t.name, CommentParser.parse(className, t.value))); - } - } - return new ClassJavadoc(className, CommentParser.parse(className, parsed.getDescription()), fields, enumConstants, methods, - constructors, otherDocs, seeAlsoDocs, paramDocs); + ParsedJavadoc parsed = parse(javadoc, className); + return new ClassJavadoc(className, parsed.getDescription(), fields, enumConstants, methods, + constructors, parsed.getOtherDocs(), parsed.getSeeAlsoDocs(), parsed.getParamDocs()); } public static FieldJavadoc parseFieldJavadoc(String owningClass, String fieldName, String javadoc) { - ParsedJavadoc parsed = parse(javadoc); - - List otherDocs = new ArrayList<>(); - List seeAlsoDocs = new ArrayList<>(); - - for (BlockTag t : parsed.getBlockTags()) { - if (t.name.equals("see")) { - SeeAlsoJavadoc seeAlso = SeeAlsoParser.parseSeeAlso(owningClass, t.value); - if (seeAlso != null) { - seeAlsoDocs.add(seeAlso); - } - } else { - otherDocs.add(new OtherJavadoc(t.name, CommentParser.parse(owningClass, t.value))); - } - } - - return new FieldJavadoc(fieldName, CommentParser.parse(owningClass, parsed.getDescription()), otherDocs, seeAlsoDocs); + ParsedJavadoc parsed = parse(javadoc, owningClass); + return new FieldJavadoc(fieldName, parsed.getDescription(), parsed.getOtherDocs(), parsed.getSeeAlsoDocs()); } public static MethodJavadoc parseMethodJavadoc(String owningClass, String methodName, List paramTypes, String javadoc) { - ParsedJavadoc parsed = parse(javadoc); - - List otherDocs = new ArrayList<>(); - List seeAlsoDocs = new ArrayList<>(); - List paramDocs = new ArrayList<>(); - List throwsDocs = new ArrayList<>(); - - Comment returns = null; - - for (BlockTag t : parsed.getBlockTags()) { - if (t.name.equals("param")) { - paramDocs.add(parseParam(t, owningClass)); - } else if (t.name.equals("return")) { - returns = CommentParser.parse(owningClass, t.value); - } else if (t.name.equals("see")) { - SeeAlsoJavadoc seeAlso = SeeAlsoParser.parseSeeAlso(owningClass, t.value); - if (seeAlso != null) { - seeAlsoDocs.add(seeAlso); - } - } else if (t.name.equals("throws") || t.name.equals("exception")) { - ThrowsJavadoc throwsDoc = ThrowsTagParser.parseTag(owningClass, t.value); - if (throwsDoc != null) { - throwsDocs.add(throwsDoc); - } - } else { - otherDocs.add(new OtherJavadoc(t.name, CommentParser.parse(owningClass, t.value))); - } - } - - return new MethodJavadoc(methodName, paramTypes, CommentParser.parse(owningClass, parsed.getDescription()), paramDocs, - throwsDocs, otherDocs, returns, seeAlsoDocs); + ParsedJavadoc parsed = parse(javadoc, owningClass); + return new MethodJavadoc(methodName, paramTypes, parsed.getDescription(), parsed.getParamDocs(), + parsed.getThrowsDocs(), parsed.getOtherDocs(), parsed.getReturns(), parsed.getSeeAlsoDocs()); } - private static ParsedJavadoc parse(String javadoc) { + private static ParsedJavadoc parse(String javadoc, String owningClass) { String[] blocks = blockSeparator.split(javadoc); - ParsedJavadoc result = new ParsedJavadoc(); - result.description = blocks[0].trim(); + List paramDocs = new ArrayList<>(); + List seeAlsoDocs = new ArrayList<>(); + List throwsDocs = new ArrayList<>(); + List otherDocs = new ArrayList<>(); + Comment returns = null; for (int i = 1; i < blocks.length; i++) { - result.blockTags.add(parseBlockTag(blocks[i])); + BlockTag blockTag = parseBlockTag(blocks[i]); + switch (blockTag.name) { + case "param": + paramDocs.add(parseParam(blockTag, owningClass)); + break; + case "return": + returns = CommentParser.parse(owningClass, blockTag.value); + break; + case "see": + SeeAlsoJavadoc seeAlso = SeeAlsoParser.parseSeeAlso(owningClass, blockTag.value); + if (seeAlso != null) { + seeAlsoDocs.add(seeAlso); + } + break; + case "throws": + case "exception": + ThrowsJavadoc throwsDoc = ThrowsTagParser.parseTag(owningClass, blockTag.value); + if (throwsDoc != null) { + throwsDocs.add(throwsDoc); + } + break; + default: + otherDocs.add(new OtherJavadoc(blockTag.name, CommentParser.parse(owningClass, blockTag.value))); + break; + } } - return result; + Comment description = CommentParser.parse(owningClass, blocks[0].trim()); + return new ParsedJavadoc(description, otherDocs, seeAlsoDocs, paramDocs, throwsDocs, returns); } private static BlockTag parseBlockTag(String block) { diff --git a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/ParsedJavadoc.java b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/ParsedJavadoc.java index df3960b..ed8f879 100644 --- a/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/ParsedJavadoc.java +++ b/therapi-runtime-javadoc/src/main/java/com/github/therapi/runtimejavadoc/internal/parser/ParsedJavadoc.java @@ -16,28 +16,67 @@ package com.github.therapi.runtimejavadoc.internal.parser; +import com.github.therapi.runtimejavadoc.Comment; +import com.github.therapi.runtimejavadoc.OtherJavadoc; +import com.github.therapi.runtimejavadoc.ParamJavadoc; +import com.github.therapi.runtimejavadoc.SeeAlsoJavadoc; +import com.github.therapi.runtimejavadoc.ThrowsJavadoc; +import static com.github.therapi.runtimejavadoc.internal.RuntimeJavadocHelper.unmodifiableDefensiveCopy; import java.util.ArrayList; import java.util.List; -public class ParsedJavadoc { +class ParsedJavadoc { - String description; + private final Comment description; + private final List otherDocs; + private final List seeAlsoDocs; + private final List paramDocs; + private final List throwsDocs; + private final Comment returns; - List blockTags = new ArrayList<>(); + ParsedJavadoc(Comment description, List otherDocs, List seeAlsoDocs, + List paramDocs, List throwsDocs, Comment returns) { + this.description = description; + this.otherDocs = unmodifiableDefensiveCopy(otherDocs); + this.seeAlsoDocs = unmodifiableDefensiveCopy(seeAlsoDocs); + this.paramDocs = unmodifiableDefensiveCopy(paramDocs); + this.throwsDocs = unmodifiableDefensiveCopy(throwsDocs); + this.returns = Comment.nullToEmpty(returns); + } @Override public String toString() { return "ParsedJavadoc{" + "description='" + description + '\'' + - ", blockTags=" + blockTags + + ", otherDocs=" + otherDocs + + ", seeAlsoDocs=" + seeAlsoDocs + + ", paramDocs=" + paramDocs + + ", throwsDocs=" + throwsDocs + + ", returns=" + returns + '}'; } - String getDescription() { + Comment getDescription() { return description; } - List getBlockTags() { - return blockTags; + List getOtherDocs() { + return otherDocs; + } + + List getSeeAlsoDocs() { + return seeAlsoDocs; + } + + List getParamDocs() { + return paramDocs; + } + + List getThrowsDocs() { + return throwsDocs; + } + + Comment getReturns() { + return returns; } }