diff --git a/MANUAL.adoc b/MANUAL.adoc index 1d90585..ee49139 100644 --- a/MANUAL.adoc +++ b/MANUAL.adoc @@ -6075,6 +6075,270 @@ public class JavadocAutoParam { This generates `@param `, `@param first`, and `@param second` tags automatically from the individual `docComment` callbacks. +=== Co-located @return and @throws Documentation + +Just as parameters and type parameters support co-located documentation via their creator callbacks, the `returning`, `returningInline`, and `throws_` methods on `MethodCreator` (and `throws_` on `ConstructorCreator`) accept an optional `Consumer` that generates the corresponding Javadoc tag automatically. + +This is the recommended approach because it keeps documentation next to the declaration it describes, rather than collecting all tags inside a separate `docComment` callback. + +==== @return via `returning` + +//TEST:BEGIN +[source,java] +---- +import java.io.IOException; +import java.nio.file.Path; + +import io.smallrye.jdeparser.Expr; +import io.smallrye.jdeparser.JDeparser; +import io.smallrye.jdeparser.Sources; +import io.smallrye.jdeparser.SourceVersion; +import io.smallrye.jdeparser.Type; +import io.smallrye.jdeparser.Var; +import io.smallrye.jdeparser.format.Filer; +import io.smallrye.jdeparser.format.FormatPreferences; + +public class JavadocColocatedReturn { + public static void main(String[] args) throws IOException { + Filer filer = Filer.newInstance(Path.of("generated-sources")); + Sources sources = JDeparser.createSources( + filer, FormatPreferences.defaults(), SourceVersion.JAVA_17 + ); + + sources.createSourceFile("com.example", "Example", sf -> { + sf.class_("Example", cc -> { + cc.method("getName", mc -> { + mc.public_(); + // documentation is co-located with the return type declaration + mc.returning(Type.STRING, doc -> { + doc.text("the name of this entity, never "); + doc.code("null"); + }); + mc.body(body -> { + body.return_(Expr.$v("name")); + }); + }); + }); + }); + + sources.writeSources(); + } +} +---- +//TEST:END + +This generates: + +[source,java] +---- +/** + * @return the name of this entity, never {@code null} + */ +public String getName() { +---- + +==== {@return} via `returningInline` (Java 16+) + +The `returningInline` method generates the inline `{@return ...}` tag, which serves as both the first summary sentence and the `@return` tag: + +//TEST:BEGIN +[source,java] +---- +import java.io.IOException; +import java.nio.file.Path; + +import io.smallrye.jdeparser.Expr; +import io.smallrye.jdeparser.JDeparser; +import io.smallrye.jdeparser.Sources; +import io.smallrye.jdeparser.SourceVersion; +import io.smallrye.jdeparser.Type; +import io.smallrye.jdeparser.format.Filer; +import io.smallrye.jdeparser.format.FormatPreferences; + +public class JavadocColocatedReturnInline { + public static void main(String[] args) throws IOException { + Filer filer = Filer.newInstance(Path.of("generated-sources")); + Sources sources = JDeparser.createSources( + filer, FormatPreferences.defaults(), SourceVersion.JAVA_17 + ); + + sources.createSourceFile("com.example", "Example", sf -> { + sf.class_("Example", cc -> { + cc.method("getCount", mc -> { + mc.public_(); + mc.returningInline(Type.INT, doc -> { + doc.text("the current count of processed items"); + }); + mc.body(body -> { + body.return_(Expr.$v("count")); + }); + }); + }); + }); + + sources.writeSources(); + } +} +---- +//TEST:END + +This generates: + +[source,java] +---- +/** + * {@return the current count of processed items} + */ +public int getCount() { +---- + +==== @throws via `throws_` + +//TEST:BEGIN +[source,java] +---- +import java.io.IOException; +import java.nio.file.Path; + +import io.smallrye.jdeparser.Expr; +import io.smallrye.jdeparser.JDeparser; +import io.smallrye.jdeparser.Sources; +import io.smallrye.jdeparser.SourceVersion; +import io.smallrye.jdeparser.Type; +import io.smallrye.jdeparser.Var; +import io.smallrye.jdeparser.format.Filer; +import io.smallrye.jdeparser.format.FormatPreferences; + +public class JavadocColocatedThrows { + public static void main(String[] args) throws IOException { + Filer filer = Filer.newInstance(Path.of("generated-sources")); + Sources sources = JDeparser.createSources( + filer, FormatPreferences.defaults(), SourceVersion.JAVA_17 + ); + + sources.createSourceFile("com.example", "Example", sf -> { + sf.class_("Example", cc -> { + cc.method("parse", mc -> { + mc.public_(); + mc.returning(Type.INT); + Var input = mc.param("input", Type.STRING); + // documentation is co-located with each throws declaration + mc.throws_(Type.named("java.lang.NumberFormatException"), doc -> { + doc.text("if "); + doc.code("input"); + doc.text(" is not a valid integer"); + }); + mc.throws_(Type.named("java.lang.IllegalArgumentException"), doc -> { + doc.text("if "); + doc.code("input"); + doc.text(" is "); + doc.code("null"); + }); + mc.body(body -> { + body.return_(Type.named("java.lang.Integer") + .call("parseInt", input)); + }); + }); + }); + }); + + sources.writeSources(); + } +} +---- +//TEST:END + +This generates: + +[source,java] +---- +/** + * @throws NumberFormatException if {@code input} is not a valid integer + * @throws IllegalArgumentException if {@code input} is {@code null} + */ +public int parse(String input) throws NumberFormatException, IllegalArgumentException { +---- + +==== Combining co-located documentation + +All co-located documentation methods (`param` with doc, `typeParam` with doc, `returning` with doc, `throws_` with doc) can be used together, and can also be combined with a `docComment` callback for the method description and other block tags. +Tags appear in declaration order within each category. + +//TEST:BEGIN +[source,java] +---- +import java.io.IOException; +import java.nio.file.Path; + +import io.smallrye.jdeparser.Expr; +import io.smallrye.jdeparser.JDeparser; +import io.smallrye.jdeparser.Sources; +import io.smallrye.jdeparser.SourceVersion; +import io.smallrye.jdeparser.Type; +import io.smallrye.jdeparser.Var; +import io.smallrye.jdeparser.format.Filer; +import io.smallrye.jdeparser.format.FormatPreferences; + +public class JavadocColocatedCombined { + public static void main(String[] args) throws IOException { + Filer filer = Filer.newInstance(Path.of("generated-sources")); + Sources sources = JDeparser.createSources( + filer, FormatPreferences.defaults(), SourceVersion.JAVA_17 + ); + + sources.createSourceFile("com.example", "Example", sf -> { + sf.class_("Example", cc -> { + cc.method("transform", mc -> { + mc.public_(); + Type t = mc.typeParam("T", tp -> { + tp.docComment(doc -> doc.text("the element type")); + }); + mc.returning(Type.named("java.util.List").typeArg(t), doc -> { + doc.text("a new list containing the transformed elements"); + }); + Var input = mc.param("input", Type.named("java.util.List").typeArg(t), pc -> { + pc.docComment(doc -> doc.text("the elements to transform")); + }); + mc.throws_(Type.named("java.lang.IllegalArgumentException"), doc -> { + doc.text("if "); + doc.code("input"); + doc.text(" is "); + doc.code("null"); + doc.text(" or empty"); + }); + mc.docComment(doc -> { + doc.text("Transforms a list of elements."); + doc.since("2.0"); + }); + mc.body(body -> { + body.return_(input); + }); + }); + }); + }); + + sources.writeSources(); + } +} +---- +//TEST:END + +This generates: + +[source,java] +---- +/** + * Transforms a list of elements. + * + * @param the element type + * @param input the elements to transform + * @return a new list containing the transformed elements + * @throws IllegalArgumentException if {@code input} is {@code null} or empty + * @since 2.0 + */ +public List transform(List input) throws IllegalArgumentException { +---- + === Complete Javadoc Example //TEST:BEGIN @@ -6138,7 +6402,11 @@ public class JavadocExample { Type t = mc.typeParam("T", tp -> { tp.docComment(doc -> doc.text("the record type")); }); - mc.returning(listType.typeArg(t)); + mc.returning(listType.typeArg(t), doc -> { + doc.text("a new list containing the processed records, "); + doc.text("never "); + doc.code("null"); + }); Var records = mc.param("records", listType.typeArg(t), pc -> { pc.docComment(doc -> { doc.text("the records to process (must not be "); @@ -6146,17 +6414,12 @@ public class JavadocExample { doc.text(")"); }); }); - mc.throws_(Type.named("java.lang.IllegalArgumentException")); + mc.throws_(Type.named("java.lang.IllegalArgumentException"), doc -> { + doc.text("if records is null or exceeds the maximum batch size"); + }); mc.docComment(doc -> { doc.text("Processes a list of records and returns the results."); - doc.return_(c -> { - c.text("a new list containing the processed records, "); - c.text("never "); - c.code("null"); - }); - doc.throws_(Type.named("java.lang.IllegalArgumentException"), - "if records is null or exceeds the maximum batch size"); doc.since("1.0"); }); diff --git a/src/main/java/io/smallrye/jdeparser/creator/ConstructorCreator.java b/src/main/java/io/smallrye/jdeparser/creator/ConstructorCreator.java index 616d4f4..2322496 100644 --- a/src/main/java/io/smallrye/jdeparser/creator/ConstructorCreator.java +++ b/src/main/java/io/smallrye/jdeparser/creator/ConstructorCreator.java @@ -49,6 +49,17 @@ public sealed interface ConstructorCreator extends ModifiableCreator permits Con */ void throws_(Type exceptionType); + /** + * Adds a thrown exception type to this constructor with documentation. + *

+ * The documentation provided by the builder is contributed as a + * {@code @throws} tag in this constructor's Javadoc comment. + * + * @param exceptionType the exception type + * @param builder the callback to provide the {@code @throws} tag content + */ + void throws_(Type exceptionType, Consumer builder); + /** * Adds a type parameter to this constructor. * diff --git a/src/main/java/io/smallrye/jdeparser/creator/MethodCreator.java b/src/main/java/io/smallrye/jdeparser/creator/MethodCreator.java index 58a3b5c..3234ec6 100644 --- a/src/main/java/io/smallrye/jdeparser/creator/MethodCreator.java +++ b/src/main/java/io/smallrye/jdeparser/creator/MethodCreator.java @@ -2,6 +2,7 @@ import java.util.function.Consumer; +import io.smallrye.jdeparser.SourceVersion; import io.smallrye.jdeparser.Type; import io.smallrye.jdeparser.Var; import io.smallrye.jdeparser.impl.MethodCreatorImpl; @@ -22,6 +23,33 @@ public sealed interface MethodCreator extends ModifiableCreator permits MethodCr */ void returning(Type type); + /** + * Sets the return type of this method with documentation. + *

+ * The documentation provided by the builder is contributed as a + * {@code @return} block tag in this method's Javadoc comment. + * + * @param type the return type (use {@link Type#VOID} for void methods) + * @param builder the callback to provide the {@code @return} tag content + */ + void returning(Type type, Consumer builder); + + /** + * Sets the return type of this method with inline return documentation. + *

+ * The documentation provided by the builder is contributed as an + * inline {@code {@return ...}} tag in this method's Javadoc comment, + * which serves as both the first summary sentence and the {@code @return} + * block tag. + *

+ * Requires source version {@linkplain SourceVersion#JAVA_16 16} + * or later. + * + * @param type the return type (use {@link Type#VOID} for void methods) + * @param builder the callback to provide the {@code {@return}} tag content + */ + void returningInline(Type type, Consumer builder); + /** * Adds a parameter to this method (simple form). * @@ -58,6 +86,17 @@ public sealed interface MethodCreator extends ModifiableCreator permits MethodCr */ void throws_(Type exceptionType); + /** + * Adds a thrown exception type to this method with documentation. + *

+ * The documentation provided by the builder is contributed as a + * {@code @throws} tag in this method's Javadoc comment. + * + * @param exceptionType the exception type + * @param builder the callback to provide the {@code @throws} tag content + */ + void throws_(Type exceptionType, Consumer builder); + /** * Adds a type parameter to this method. * diff --git a/src/main/java/io/smallrye/jdeparser/impl/ConstructorCreatorImpl.java b/src/main/java/io/smallrye/jdeparser/impl/ConstructorCreatorImpl.java index d32a326..2a75627 100644 --- a/src/main/java/io/smallrye/jdeparser/impl/ConstructorCreatorImpl.java +++ b/src/main/java/io/smallrye/jdeparser/impl/ConstructorCreatorImpl.java @@ -14,6 +14,7 @@ import io.smallrye.jdeparser.creator.BlockCreator; import io.smallrye.jdeparser.creator.ConstructorCreator; import io.smallrye.jdeparser.creator.DocCommentCreator; +import io.smallrye.jdeparser.creator.DocInlineCreator; import io.smallrye.jdeparser.creator.ModifierFlag; import io.smallrye.jdeparser.creator.ModifierLocation; import io.smallrye.jdeparser.creator.ParamCreator; @@ -140,6 +141,20 @@ public void throws_(final Type exceptionType) { throwsTypes.add(exceptionType); } + /** {@inheritDoc} */ + @Override + public void throws_(final Type exceptionType, final Consumer builder) { + checkActive(); + Assert.checkNotNullParam("exceptionType", exceptionType); + Assert.checkNotNullParam("builder", builder); + registerUsedType(exceptionType); + throwsTypes.add(exceptionType); + final DocInlineCreatorImpl dc = new DocInlineCreatorImpl(version(), sourceFile(), null); + nest(() -> builder.accept(dc)); + dc.finish(); + getOrCreateDocComment().addThrowsTag(exceptionType, dc); + } + /** {@inheritDoc} */ @Override public Type typeParam(final String name, final Consumer builder) { diff --git a/src/main/java/io/smallrye/jdeparser/impl/DocCommentCreatorImpl.java b/src/main/java/io/smallrye/jdeparser/impl/DocCommentCreatorImpl.java index c09c0d6..d2afb78 100644 --- a/src/main/java/io/smallrye/jdeparser/impl/DocCommentCreatorImpl.java +++ b/src/main/java/io/smallrye/jdeparser/impl/DocCommentCreatorImpl.java @@ -25,6 +25,11 @@ * {@code @param} tags for method parameters, type parameters, and record * components are aggregated from sub-creators and added via * {@link #addParamTag(String, DocInlineCreatorImpl)} and {@link #addTypeParamTag(String, DocInlineCreatorImpl)}. + * Similarly, {@code @return} and {@code @throws} tags can be aggregated + * from the enclosing method/constructor creator via + * {@link #addReturnTag(DocInlineCreatorImpl)}, + * {@link #addReturnInlineTag(DocInlineCreatorImpl)}, and + * {@link #addThrowsTag(Type, DocInlineCreatorImpl)}. *

* Type names in {@code {@link}}, {@code {@linkplain}}, {@code @throws}, * and {@code @see} tags are resolved at write time via the @@ -318,6 +323,73 @@ public void addTypeParamTag(final String typeParamName, final DocInlineCreatorIm }); } + /** + * Adds a {@code @return} block tag from the enclosing method creator. + *

+ * This method is called eagerly by the enclosing method creator + * when a {@code returning} call includes documentation, so that + * write-time aggregation is not needed. + * + * @param description the inline content for the return description + */ + public void addReturnTag(final DocInlineCreatorImpl description) { + blockTags.add(w -> { + w.writeUnescaped("@return"); + if (description != null && description.hasInlineContent()) { + w.ntsp(); + description.writeInline(w); + } + }); + } + + /** + * Adds a {@code {@return ...}} inline tag from the enclosing method creator. + *

+ * This method is called eagerly by the enclosing method creator + * when a {@code returningInline} call includes documentation. + * The content serves as both the first summary sentence and the + * {@code @return} tag. + *

+ * Requires source version {@linkplain io.smallrye.jdeparser.SourceVersion#JAVA_16 16} + * or later. + * + * @param description the inline content for the return description + */ + public void addReturnInlineTag(final DocInlineCreatorImpl description) { + version().require(LanguageFeature.DOC_RETURN_INLINE); + parts.add(w -> { + w.writeUnescaped("{@return "); + if (description != null && description.hasInlineContent()) { + description.writeInline(w); + } + w.writeUnescaped("}"); + }); + } + + /** + * Adds a {@code @throws} block tag from the enclosing method or constructor creator. + *

+ * This method is called eagerly by the enclosing method/constructor + * creator when a {@code throws_} call includes documentation, so that + * write-time aggregation is not needed. + *

+ * The exception type name is resolved at write time via the + * {@linkplain SourceFileWriter#resolveClassName(String) class name resolver}. + * + * @param exceptionType the exception type + * @param description the inline content for the exception description + */ + public void addThrowsTag(final Type exceptionType, final DocInlineCreatorImpl description) { + final String qualifiedName = typeName(exceptionType); + blockTags.add(w -> { + w.writeUnescaped("@throws " + w.resolveClassName(qualifiedName)); + if (description != null && description.hasInlineContent()) { + w.ntsp(); + description.writeInline(w); + } + }); + } + /** * Returns whether this doc comment has any content. * diff --git a/src/main/java/io/smallrye/jdeparser/impl/MethodCreatorImpl.java b/src/main/java/io/smallrye/jdeparser/impl/MethodCreatorImpl.java index b9da877..dd22da1 100644 --- a/src/main/java/io/smallrye/jdeparser/impl/MethodCreatorImpl.java +++ b/src/main/java/io/smallrye/jdeparser/impl/MethodCreatorImpl.java @@ -13,6 +13,7 @@ import io.smallrye.jdeparser.creator.AnnotationCreator; import io.smallrye.jdeparser.creator.BlockCreator; import io.smallrye.jdeparser.creator.DocCommentCreator; +import io.smallrye.jdeparser.creator.DocInlineCreator; import io.smallrye.jdeparser.creator.MethodCreator; import io.smallrye.jdeparser.creator.ModifierFlag; import io.smallrye.jdeparser.creator.ModifierLocation; @@ -89,6 +90,34 @@ public void returning(final Type type) { this.returnType = type; } + /** {@inheritDoc} */ + @Override + public void returning(final Type type, final Consumer builder) { + checkActive(); + Assert.checkNotNullParam("type", type); + Assert.checkNotNullParam("builder", builder); + registerUsedType(type); + this.returnType = type; + final DocInlineCreatorImpl dc = new DocInlineCreatorImpl(version(), sourceFile(), null); + nest(() -> builder.accept(dc)); + dc.finish(); + getOrCreateDocComment().addReturnTag(dc); + } + + /** {@inheritDoc} */ + @Override + public void returningInline(final Type type, final Consumer builder) { + checkActive(); + Assert.checkNotNullParam("type", type); + Assert.checkNotNullParam("builder", builder); + registerUsedType(type); + this.returnType = type; + final DocInlineCreatorImpl dc = new DocInlineCreatorImpl(version(), sourceFile(), null); + nest(() -> builder.accept(dc)); + dc.finish(); + getOrCreateDocComment().addReturnInlineTag(dc); + } + /** {@inheritDoc} */ @Override public Var param(final String name, final Type type) { @@ -150,6 +179,20 @@ public void throws_(final Type exceptionType) { throwsTypes.add(exceptionType); } + /** {@inheritDoc} */ + @Override + public void throws_(final Type exceptionType, final Consumer builder) { + checkActive(); + Assert.checkNotNullParam("exceptionType", exceptionType); + Assert.checkNotNullParam("builder", builder); + registerUsedType(exceptionType); + throwsTypes.add(exceptionType); + final DocInlineCreatorImpl dc = new DocInlineCreatorImpl(version(), sourceFile(), null); + nest(() -> builder.accept(dc)); + dc.finish(); + getOrCreateDocComment().addThrowsTag(exceptionType, dc); + } + /** {@inheritDoc} */ @Override public Type typeParam(final String name, final Consumer builder) { diff --git a/src/test/java/io/smallrye/jdeparser/test/DocCommentTest.java b/src/test/java/io/smallrye/jdeparser/test/DocCommentTest.java index 50f71fe..1d980bc 100644 --- a/src/test/java/io/smallrye/jdeparser/test/DocCommentTest.java +++ b/src/test/java/io/smallrye/jdeparser/test/DocCommentTest.java @@ -1481,4 +1481,312 @@ void returnInlineRejectedBeforeJava16() { }); }); } + + // ---- Tests for co-located returning/throws documentation ---- + + /** + * Verifies that the {@code returning(Type, Consumer)} overload generates + * a {@code @return} block tag in the method's Javadoc. + * + * @throws IOException if source generation fails + */ + @Test + void returningWithDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + sources.createSourceFile("com.example", "ReturningDoc", sf -> { + sf.class_("ReturningDoc", cc -> { + cc.public_(); + cc.method("getValue", mc -> { + mc.public_(); + mc.returning(Type.INT, dc -> dc.text("the computed value")); + mc.body(b -> { + b.return_(Expr.ZERO); + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ReturningDoc"); + assertTrue(source.contains("@return"), "should contain @return tag"); + assertTrue(source.contains("the computed value"), "should contain return description"); + } + + /** + * Verifies that the {@code returningInline(Type, Consumer)} overload generates + * an inline {@code {@return ...}} tag in the method's Javadoc. + * + * @throws IOException if source generation fails + */ + @Test + void returningInlineWithDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + sources.createSourceFile("com.example", "ReturningInlineDoc", sf -> { + sf.class_("ReturningInlineDoc", cc -> { + cc.public_(); + cc.method("getName", mc -> { + mc.public_(); + mc.returningInline(Type.STRING, dc -> dc.text("the name")); + mc.body(b -> { + b.return_(Expr.str("")); + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ReturningInlineDoc"); + assertTrue(source.contains("{@return the name}"), "should contain inline {@return} tag"); + } + + /** + * Verifies that the {@code returningInline} overload requires Java 16+. + */ + @Test + void returningInlineDocRejectedBeforeJava16() { + final Sources sources = createSources(SourceVersion.JAVA_15); + assertThrows(IllegalStateException.class, () -> { + sources.createSourceFile("com.example", "ReturningInlineBad", sf -> { + sf.class_("ReturningInlineBad", cc -> { + cc.public_(); + cc.method("foo", mc -> { + mc.public_(); + mc.returningInline(Type.INT, dc -> dc.text("the value")); + mc.body(b -> { + b.return_(Expr.ZERO); + }); + }); + }); + }); + }); + } + + /** + * Verifies that the {@code throws_(Type, Consumer)} overload generates + * a {@code @throws} block tag in the method's Javadoc. + * + * @throws IOException if source generation fails + */ + @Test + void throwsWithDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + final Type ioException = Type.named("java.io.IOException"); + sources.createSourceFile("com.example", "ThrowsWithDoc", sf -> { + sf.class_("ThrowsWithDoc", cc -> { + cc.public_(); + cc.method("readFile", mc -> { + mc.public_(); + mc.throws_(ioException, dc -> dc.text("if I/O fails")); + mc.body(b -> { + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ThrowsWithDoc"); + assertTrue(source.contains("@throws java.io.IOException"), "should contain @throws tag with type"); + assertTrue(source.contains("if I/O fails"), "should contain throws description"); + assertTrue(source.contains("throws java.io.IOException"), + "should contain throws clause in method signature"); + } + + /** + * Verifies that the {@code throws_(Type, Consumer)} overload on a constructor + * generates a {@code @throws} block tag in the constructor's Javadoc. + * + * @throws IOException if source generation fails + */ + @Test + void constructorThrowsWithDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + final Type illegalArg = Type.named("java.lang.IllegalArgumentException"); + sources.createSourceFile("com.example", "CtorThrowsDoc", sf -> { + sf.class_("CtorThrowsDoc", cc -> { + cc.public_(); + cc.constructor(ctor -> { + ctor.public_(); + ctor.param("name", Type.STRING); + ctor.throws_(illegalArg, dc -> dc.text("if name is null")); + ctor.body(b -> { + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "CtorThrowsDoc"); + assertTrue(source.contains("@throws IllegalArgumentException"), + "should contain @throws tag with resolved type"); + assertTrue(source.contains("if name is null"), "should contain throws description"); + } + + /** + * Verifies that the {@code returning} doc overload supports rich inline content + * including {@code {@code}} and {@code {@link}} tags. + * + * @throws IOException if source generation fails + */ + @Test + void returningDocWithRichContent() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + sources.createSourceFile("com.example", "ReturningRich", sf -> { + sf.class_("ReturningRich", cc -> { + cc.public_(); + cc.method("findName", mc -> { + mc.public_(); + mc.returning(Type.STRING, dc -> { + dc.text("the name, or "); + dc.code("null"); + dc.text(" if not found"); + }); + mc.body(b -> { + b.return_(Expr.NULL); + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ReturningRich"); + assertTrue(source.contains("@return"), "should contain @return tag"); + assertTrue(source.contains("the name, or {@code null} if not found"), + "should contain rich inline content in @return tag"); + } + + /** + * Verifies that the {@code throws_} doc overload supports rich inline content. + * + * @throws IOException if source generation fails + */ + @Test + void throwsDocWithRichContent() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + final Type ioException = Type.named("java.io.IOException"); + sources.createSourceFile("com.example", "ThrowsRich", sf -> { + sf.class_("ThrowsRich", cc -> { + cc.public_(); + cc.method("readFile", mc -> { + mc.public_(); + mc.throws_(ioException, dc -> { + dc.text("if the file is not "); + dc.code("readable"); + }); + mc.body(b -> { + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ThrowsRich"); + assertTrue(source.contains("@throws java.io.IOException if the file is not {@code readable}"), + "should contain @throws tag with rich inline content"); + } + + /** + * Verifies that using the {@code returning} doc overload without an explicit + * {@code docComment()} call still produces a valid Javadoc comment + * containing the {@code @return} tag. + * + * @throws IOException if source generation fails + */ + @Test + void returningDocAlone() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + sources.createSourceFile("com.example", "ReturnAlone", sf -> { + sf.class_("ReturnAlone", cc -> { + cc.public_(); + cc.method("getCount", mc -> { + mc.public_(); + mc.returning(Type.INT, dc -> dc.text("the count")); + mc.body(b -> { + b.return_(Expr.ZERO); + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "ReturnAlone"); + assertTrue(source.contains("/**"), "should produce a doc comment"); + assertTrue(source.contains("@return"), "should contain @return tag"); + assertTrue(source.contains("the count"), "should contain return description"); + assertTrue(source.contains("*/"), "should close the doc comment"); + } + + /** + * Verifies that the {@code returning} and {@code throws_} doc overloads + * combine correctly with an explicit method-level {@code docComment()} call, + * producing body text followed by block tags in call order. + * + * @throws IOException if source generation fails + */ + @Test + void returningAndThrowsDocCombinedWithMethodDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + final Type ioException = Type.named("java.io.IOException"); + sources.createSourceFile("com.example", "CombinedDoc", sf -> { + sf.class_("CombinedDoc", cc -> { + cc.public_(); + cc.method("process", mc -> { + mc.public_(); + mc.docComment(dc -> { + dc.text("Processes data."); + }); + mc.returning(Type.STRING, dc -> dc.text("the processed result")); + mc.param("input", Type.STRING, p -> { + p.docComment(dc -> dc.text("the input data")); + }); + mc.throws_(ioException, dc -> dc.text("if processing fails")); + mc.body(b -> { + b.return_(Expr.$v("input")); + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "CombinedDoc"); + assertTrue(source.contains("Processes data."), "should contain body text"); + assertTrue(source.contains("@return"), "should contain @return tag"); + assertTrue(source.contains("the processed result"), "should contain return description"); + assertTrue(source.contains("@param input"), "should contain @param tag"); + assertTrue(source.contains("@throws java.io.IOException"), "should contain @throws tag"); + + // Verify ordering: body text before all block tags + final int textIndex = source.indexOf("Processes data."); + final int returnIndex = source.indexOf("@return"); + final int paramIndex = source.indexOf("@param input"); + final int throwsIndex = source.indexOf("@throws java.io.IOException"); + assertTrue(textIndex < returnIndex, "body text should appear before @return"); + assertTrue(returnIndex < paramIndex, "@return should appear before @param (call order)"); + assertTrue(paramIndex < throwsIndex, "@param should appear before @throws (call order)"); + } + + /** + * Verifies that calling {@code throws_(Type, Consumer)} multiple times + * produces multiple {@code @throws} tags in the generated Javadoc. + * + * @throws IOException if source generation fails + */ + @Test + void multipleThrowsWithDoc() throws IOException { + final Sources sources = createSources(SourceVersion.JAVA_17); + final Type ioException = Type.named("java.io.IOException"); + final Type illegalArg = Type.named("java.lang.IllegalArgumentException"); + sources.createSourceFile("com.example", "MultiThrowsDoc", sf -> { + sf.class_("MultiThrowsDoc", cc -> { + cc.public_(); + cc.method("process", mc -> { + mc.public_(); + mc.throws_(ioException, dc -> dc.text("if I/O fails")); + mc.throws_(illegalArg, dc -> dc.text("if the argument is invalid")); + mc.body(b -> { + }); + }); + }); + }); + sources.writeSources(); + final String source = getSource("com.example", "MultiThrowsDoc"); + assertTrue(source.contains("@throws java.io.IOException"), + "should contain first @throws tag"); + assertTrue(source.contains("if I/O fails"), "should contain first throws description"); + assertTrue(source.contains("@throws IllegalArgumentException"), + "should contain second @throws tag"); + assertTrue(source.contains("if the argument is invalid"), + "should contain second throws description"); + } } diff --git a/src/test/java/io/smallrye/jdeparser/test/DocumentationExamplesTest.java b/src/test/java/io/smallrye/jdeparser/test/DocumentationExamplesTest.java index 9b057cd..31b2249 100644 --- a/src/test/java/io/smallrye/jdeparser/test/DocumentationExamplesTest.java +++ b/src/test/java/io/smallrye/jdeparser/test/DocumentationExamplesTest.java @@ -103,7 +103,7 @@ private static void compileAndRun(JavaCompiler javac, String source, String className, boolean compileOnly) throws Throwable { // rewrite output path so generated files go to target/doc-example String rewritten = source.replace( - "Path.of(\"generated-sources\")", "Path.of(\"target/doc-example\")"); + "\"generated-sources\"", "\"target/doc-example\""); String classpath = buildClasspath();