diff --git a/.gitignore b/.gitignore index 467dffd57d..d2f7806c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ out/ .settings/ .DS_Store bin/ - -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json +working-set*/ diff --git a/src/main/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocComment.java b/src/main/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocComment.java new file mode 100644 index 0000000000..f950c1b6ad --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocComment.java @@ -0,0 +1,589 @@ +/* + * Copyright 2025 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaPrinter; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; + +import java.util.*; + +public class JavadocToMarkdownDocComment extends Recipe { + + @Getter + final String displayName = "Convert Javadoc to Markdown documentation comments"; + + @Getter + final String description = "Convert traditional Javadoc comments (`/** ... */`) to Markdown documentation comments (`///`) " + + "as supported by JEP 467 in Java 23+. Transforms HTML constructs like `
`, ``, ``, ``, and lists " +
+ "to their Markdown equivalents, and converts inline tags like `{@code}` and `{@link}` to Markdown syntax.";
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(new UsesJavaVersion<>(23), new JavaIsoVisitor() {
+ @Override
+ public Space visitSpace(Space space, Space.Location loc, ExecutionContext ctx) {
+ String spaceWhitespace = space.getWhitespace();
+ return space.withComments(ListUtils.flatMap(space.getComments(), comment ->
+ comment instanceof Javadoc.DocComment ?
+ convertDocComment((Javadoc.DocComment) comment, spaceWhitespace) :
+ comment));
+ }
+ });
+ }
+
+ private static List convertDocComment(Javadoc.DocComment docComment, String spaceWhitespace) {
+ // Derive the indentation from the space whitespace (e.g., "\n " → " ")
+ int lastNewline = spaceWhitespace.lastIndexOf('\n');
+ String indentation = lastNewline >= 0 ? spaceWhitespace.substring(lastNewline + 1) : spaceWhitespace;
+
+ JavadocToMarkdownConverter converter = new JavadocToMarkdownConverter();
+ converter.convert(docComment.getBody());
+
+ // Strip one leading space (from the space after * in javadoc), right-trim, drop
+ // leading/trailing blank lines, and collapse consecutive blank lines
+ List lines = normalizeLines(converter.getLines());
+
+ if (lines.isEmpty()) {
+ lines = Collections.singletonList("");
+ }
+
+ String interLineSuffix = "\n" + indentation;
+ return ListUtils.mapLast(toTextComments(lines, interLineSuffix),
+ comment -> comment.withSuffix(docComment.getSuffix()));
+ }
+
+ private static List normalizeLines(List raw) {
+ // Strip one leading space and right-trim
+ List lines = new ArrayList<>(raw.size());
+ for (String line : raw) {
+ String stripped = line.startsWith(" ") ? line.substring(1) : line;
+ lines.add(stripped.replaceAll("\\s+$", ""));
+ }
+
+ // Trim leading and trailing blank lines
+ int start = 0;
+ while (start < lines.size() && lines.get(start).isEmpty()) {
+ start++;
+ }
+ int end = lines.size();
+ while (end > start && lines.get(end - 1).isEmpty()) {
+ end--;
+ }
+
+ // Collapse consecutive blank lines
+ List result = new ArrayList<>();
+ boolean prevBlank = false;
+ for (int i = start; i < end; i++) {
+ String line = lines.get(i);
+ boolean blank = line.isEmpty();
+ if (!blank || !prevBlank) {
+ result.add(line);
+ }
+ prevBlank = blank;
+ }
+ return result;
+ }
+
+ private static List toTextComments(List lines, String suffix) {
+ List result = new ArrayList<>(lines.size());
+ for (String lineContent : lines) {
+ // TextComment(false, text, suffix, markers) prints as "// + text"
+ // So text="/ content" produces "/// content"
+ String text = lineContent.isEmpty() ? "/" : "/ " + lineContent;
+ result.add(new TextComment(false, text, suffix, Markers.EMPTY));
+ }
+ return result;
+ }
+
+ static class JavadocToMarkdownConverter {
+ private final List lines = new ArrayList<>();
+ private StringBuilder currentLine = new StringBuilder();
+ private boolean inPre = false;
+ private final Deque listStack = new ArrayDeque<>();
+ private final Deque listCounterStack = new ArrayDeque<>();
+
+ List getLines() {
+ flushLine();
+ return lines;
+ }
+
+ private void flushLine() {
+ if (currentLine.length() > 0) {
+ lines.add(currentLine.toString());
+ currentLine = new StringBuilder();
+ } else if (lines.isEmpty()) {
+ lines.add(currentLine.toString());
+ currentLine = new StringBuilder();
+ }
+ }
+
+ void convert(List body) {
+ for (Javadoc node : body) {
+ convertNode(node);
+ }
+ }
+
+ private void convertNode(Javadoc node) {
+ if (node instanceof Javadoc.Text) {
+ currentLine.append(decodeHtmlEntities(((Javadoc.Text) node).getText()));
+ } else if (node instanceof Javadoc.LineBreak) {
+ lines.add(currentLine.toString());
+ currentLine = new StringBuilder();
+ } else if (node instanceof Javadoc.Literal) {
+ convertLiteral((Javadoc.Literal) node);
+ } else if (node instanceof Javadoc.Link) {
+ convertLink((Javadoc.Link) node);
+ } else if (node instanceof Javadoc.StartElement) {
+ convertStartElement((Javadoc.StartElement) node);
+ } else if (node instanceof Javadoc.EndElement) {
+ convertEndElement((Javadoc.EndElement) node);
+ } else if (node instanceof Javadoc.Parameter) {
+ convertParameter((Javadoc.Parameter) node);
+ } else if (node instanceof Javadoc.Return) {
+ convertReturn((Javadoc.Return) node);
+ } else if (node instanceof Javadoc.Throws) {
+ convertThrows((Javadoc.Throws) node);
+ } else if (node instanceof Javadoc.See) {
+ convertSee((Javadoc.See) node);
+ } else if (node instanceof Javadoc.Since) {
+ convertSince((Javadoc.Since) node);
+ } else if (node instanceof Javadoc.Author) {
+ convertAuthor((Javadoc.Author) node);
+ } else if (node instanceof Javadoc.Deprecated) {
+ convertDeprecated((Javadoc.Deprecated) node);
+ } else if (node instanceof Javadoc.InheritDoc) {
+ currentLine.append("{@inheritDoc}");
+ } else if (node instanceof Javadoc.Snippet) {
+ convertSnippet((Javadoc.Snippet) node);
+ } else if (node instanceof Javadoc.DocRoot) {
+ currentLine.append("{@docRoot}");
+ } else if (node instanceof Javadoc.InlinedValue) {
+ convertInlinedValue((Javadoc.InlinedValue) node);
+ } else if (node instanceof Javadoc.Version) {
+ convertVersion((Javadoc.Version) node);
+ } else if (node instanceof Javadoc.Hidden) {
+ convertHidden((Javadoc.Hidden) node);
+ } else if (node instanceof Javadoc.Index) {
+ convertIndex((Javadoc.Index) node);
+ } else if (node instanceof Javadoc.Summary) {
+ convertSummary((Javadoc.Summary) node);
+ } else if (node instanceof Javadoc.UnknownBlock) {
+ convertUnknownBlock((Javadoc.UnknownBlock) node);
+ } else if (node instanceof Javadoc.UnknownInline) {
+ convertUnknownInline((Javadoc.UnknownInline) node);
+ } else if (node instanceof Javadoc.Erroneous) {
+ currentLine.append(((Javadoc.Erroneous) node).getText());
+ } else if (node instanceof Javadoc.Reference) {
+ currentLine.append(printReference((Javadoc.Reference) node));
+ }
+ }
+
+ private void convertLiteral(Javadoc.Literal literal) {
+ String content = stripLeadingSpace(renderInline(literal.getDescription()));
+ if (literal.isCode()) {
+ if (content.contains("\n")) {
+ // Multi-line: use fenced code block
+ currentLine.append("```");
+ lines.add(currentLine.toString());
+ for (String line : content.split("\n", -1)) {
+ lines.add(line);
+ }
+ currentLine = new StringBuilder("```");
+ } else {
+ currentLine.append('`').append(content).append('`');
+ }
+ } else {
+ currentLine.append(content);
+ }
+ }
+
+ private void convertLink(Javadoc.Link link) {
+ String ref = printReference(link.getTreeReference());
+ String label = stripLeadingSpace(renderInline(link.getLabel())).trim();
+
+ if (!label.isEmpty()) {
+ currentLine.append('[').append(label).append("][").append(ref).append(']');
+ } else {
+ currentLine.append('[').append(ref).append(']');
+ }
+ }
+
+ private void convertStartElement(Javadoc.StartElement element) {
+ String name = element.getName().toLowerCase();
+ if (inPre && !"pre".equals(name)) {
+ renderHtmlStartElement(element);
+ return;
+ }
+ switch (name) {
+ case "pre":
+ inPre = true;
+ currentLine.append("```");
+ break;
+ case "code":
+ if (!inPre) {
+ currentLine.append('`');
+ }
+ break;
+ case "p":
+ // Blank line for paragraph
+ lines.add(currentLine.toString());
+ lines.add("");
+ currentLine = new StringBuilder();
+ break;
+ case "em":
+ case "i":
+ currentLine.append('_');
+ break;
+ case "strong":
+ case "b":
+ currentLine.append("**");
+ break;
+ case "ul":
+ listStack.push("ul");
+ lines.add(currentLine.toString());
+ currentLine = new StringBuilder();
+ break;
+ case "ol":
+ listStack.push("ol");
+ listCounterStack.push(1);
+ lines.add(currentLine.toString());
+ currentLine = new StringBuilder();
+ break;
+ case "li":
+ if (!listStack.isEmpty()) {
+ String listType = listStack.peek();
+ if ("ol".equals(listType)) {
+ int count = listCounterStack.pop();
+ currentLine.append(count).append(". ");
+ listCounterStack.push(count + 1);
+ } else {
+ currentLine.append("- ");
+ }
+ }
+ break;
+ default:
+ renderHtmlStartElement(element);
+ break;
+ }
+ }
+
+ private void renderHtmlStartElement(Javadoc.StartElement element) {
+ currentLine.append('<').append(element.getName());
+ for (Javadoc attr : element.getAttributes()) {
+ if (attr instanceof Javadoc.Attribute) {
+ Javadoc.Attribute a = (Javadoc.Attribute) attr;
+ currentLine.append(' ').append(a.getName());
+ List value = a.getValue();
+ if (value != null && !value.isEmpty()) {
+ currentLine.append('=').append(renderInline(value));
+ }
+ }
+ }
+ if (element.isSelfClosing()) {
+ currentLine.append('/');
+ }
+ currentLine.append('>');
+ }
+
+ private void convertEndElement(Javadoc.EndElement element) {
+ String name = element.getName().toLowerCase();
+ if (inPre && !"pre".equals(name)) {
+ currentLine.append("").append(element.getName()).append('>');
+ return;
+ }
+ switch (name) {
+ case "pre":
+ inPre = false;
+ currentLine.append("```");
+ break;
+ case "code":
+ if (!inPre) {
+ currentLine.append('`');
+ }
+ break;
+ case "em":
+ case "i":
+ currentLine.append('_');
+ break;
+ case "strong":
+ case "b":
+ currentLine.append("**");
+ break;
+ case "ul":
+ if (!listStack.isEmpty()) {
+ listStack.pop();
+ }
+ break;
+ case "ol":
+ if (!listStack.isEmpty()) {
+ listStack.pop();
+ }
+ if (!listCounterStack.isEmpty()) {
+ listCounterStack.pop();
+ }
+ break;
+ case "li":
+ // End of list item handled naturally by line breaks
+ break;
+ case "p":
+ //
is often implicit, ignore
+ break;
+ default:
+ // Pass through unknown end elements
+ currentLine.append("").append(element.getName()).append('>');
+ break;
+ }
+ }
+
+ private void convertParameter(Javadoc.Parameter param) {
+ currentLine.append("@param");
+ convert(param.getSpaceBeforeName());
+ J name = param.getName();
+ if (name != null) {
+ currentLine.append(printJ(name));
+ }
+ Javadoc.Reference nameRef = param.getNameReference();
+ if (nameRef != null && nameRef.getTree() != null && name == null) {
+ currentLine.append(printJ(nameRef.getTree()));
+ }
+ convert(param.getDescription());
+ }
+
+ private void convertReturn(Javadoc.Return ret) {
+ currentLine.append("@return");
+ convert(ret.getDescription());
+ }
+
+ private void convertThrows(Javadoc.Throws thr) {
+ currentLine.append(thr.isThrowsKeyword() ? "@throws " : "@exception ");
+ J exceptionName = thr.getExceptionName();
+ if (exceptionName != null) {
+ currentLine.append(printJ(exceptionName));
+ }
+ convert(thr.getDescription());
+ }
+
+ private void convertSee(Javadoc.See see) {
+ currentLine.append("@see");
+ for (Javadoc node : see.getSpaceBeforeTree()) {
+ convertNode(node);
+ }
+ Javadoc.Reference treeRef = see.getTreeReference();
+ if (treeRef != null) {
+ currentLine.append(printReference(treeRef));
+ } else {
+ J tree = see.getTree();
+ if (tree != null) {
+ currentLine.append(printJRef(tree));
+ }
+ }
+ convert(see.getReference());
+ }
+
+ private void convertSince(Javadoc.Since since) {
+ currentLine.append("@since");
+ convert(since.getDescription());
+ }
+
+ private void convertAuthor(Javadoc.Author author) {
+ currentLine.append("@author");
+ convert(author.getName());
+ }
+
+ private void convertDeprecated(Javadoc.Deprecated deprecated) {
+ currentLine.append("@deprecated");
+ convert(deprecated.getDescription());
+ }
+
+ private void convertSnippet(Javadoc.Snippet snippet) {
+ currentLine.append("{@snippet");
+ convert(snippet.getAttributes());
+ convert(snippet.getContent());
+ currentLine.append('}');
+ }
+
+ private void convertInlinedValue(Javadoc.InlinedValue value) {
+ currentLine.append("{@value");
+ J tree = value.getTree();
+ if (tree != null) {
+ currentLine.append(' ').append(printJ(tree));
+ }
+ currentLine.append('}');
+ }
+
+ private void convertVersion(Javadoc.Version version) {
+ currentLine.append("@version");
+ convert(version.getBody());
+ }
+
+ private void convertHidden(Javadoc.Hidden hidden) {
+ currentLine.append("@hidden");
+ convert(hidden.getBody());
+ }
+
+ private void convertIndex(Javadoc.Index index) {
+ currentLine.append("{@index");
+ convert(index.getSearchTerm());
+ convert(index.getDescription());
+ currentLine.append('}');
+ }
+
+ private void convertSummary(Javadoc.Summary summary) {
+ currentLine.append("{@summary");
+ convert(summary.getSummary());
+ currentLine.append('}');
+ }
+
+ private void convertUnknownBlock(Javadoc.UnknownBlock block) {
+ currentLine.append('@').append(block.getName());
+ convert(block.getContent());
+ }
+
+ private void convertUnknownInline(Javadoc.UnknownInline inline) {
+ currentLine.append("{@").append(inline.getName());
+ convert(inline.getContent());
+ currentLine.append('}');
+ }
+
+ private String renderInline(List body) {
+ JavadocToMarkdownConverter inlineConverter = new JavadocToMarkdownConverter();
+ inlineConverter.inPre = this.inPre;
+ inlineConverter.convert(body);
+ List inlineLines = inlineConverter.getLines();
+ return String.join("\n", inlineLines);
+ }
+
+ private static String printReference(Javadoc.@Nullable Reference ref) {
+ if (ref == null) {
+ return "";
+ }
+ J tree = ref.getTree();
+ if (tree == null) {
+ return "";
+ }
+ return printJRef(tree);
+ }
+
+ /**
+ * Print a J tree as a Javadoc-style reference (using # for members instead of .)
+ */
+ private static String printJRef(J tree) {
+ if (tree instanceof J.Identifier) {
+ return ((J.Identifier) tree).getSimpleName();
+ }
+ if (tree instanceof J.FieldAccess) {
+ J.FieldAccess fa = (J.FieldAccess) tree;
+ String target = printJRef(fa.getTarget());
+ String name = fa.getSimpleName();
+ if (target.isEmpty()) {
+ return "#" + name;
+ }
+ return target + "#" + name;
+ }
+ if (tree instanceof J.MemberReference) {
+ J.MemberReference mr = (J.MemberReference) tree;
+ return printJRef(mr.getContaining()) + "#" + printJRef(mr.getReference());
+ }
+ if (tree instanceof J.MethodInvocation) {
+ J.MethodInvocation mi = (J.MethodInvocation) tree;
+ StringBuilder sb = new StringBuilder();
+ if (mi.getSelect() != null) {
+ sb.append(printJRef(mi.getSelect()));
+ sb.append('#');
+ }
+ sb.append(mi.getSimpleName());
+ sb.append('(');
+ List args = mi.getArguments();
+ for (int i = 0; i < args.size(); i++) {
+ if (i > 0) sb.append(", ");
+ Expression arg = args.get(i);
+ if (!(arg instanceof J.Empty)) {
+ sb.append(printJRef(arg));
+ }
+ }
+ sb.append(')');
+ return sb.toString();
+ }
+ return printJ(tree);
+ }
+
+ private static String printJ(J tree) {
+ if (tree instanceof J.Identifier) {
+ return ((J.Identifier) tree).getSimpleName();
+ }
+ if (tree instanceof J.FieldAccess) {
+ J.FieldAccess fa = (J.FieldAccess) tree;
+ return printJ(fa.getTarget()) + "." + fa.getSimpleName();
+ }
+ if (tree instanceof J.MemberReference) {
+ J.MemberReference mr = (J.MemberReference) tree;
+ return printJ(mr.getContaining()) + "#" + printJ(mr.getReference());
+ }
+ if (tree instanceof J.ParameterizedType) {
+ J.ParameterizedType pt = (J.ParameterizedType) tree;
+ StringBuilder sb = new StringBuilder(printJ(pt.getClazz()));
+ if (pt.getTypeParameters() != null) {
+ sb.append('<');
+ for (int i = 0; i < pt.getTypeParameters().size(); i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(printJ((J) pt.getTypeParameters().get(i)));
+ }
+ sb.append('>');
+ }
+ return sb.toString();
+ }
+ if (tree instanceof J.ArrayType) {
+ return printJ(((J.ArrayType) tree).getElementType()) + "[]";
+ }
+ if (tree instanceof J.Primitive) {
+ return ((J.Primitive) tree).getType().getKeyword();
+ }
+ try {
+ return tree.print(new JavaPrinter<>()).trim();
+ } catch (Exception e) {
+ return tree.toString();
+ }
+ }
+
+ private static String stripLeadingSpace(String s) {
+ return s.startsWith(" ") ? s.substring(1) : s;
+ }
+
+ private static String decodeHtmlEntities(String text) {
+ if (!text.contains("&")) {
+ return text;
+ }
+ return text
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("&", "&")
+ .replace(""", "\"")
+ .replace("'", "'")
+ .replace(" ", " ")
+ .replace("@", "@");
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv
index 6d4792219b..d6b45fa9f0 100644
--- a/src/main/resources/META-INF/rewrite/recipes.csv
+++ b/src/main/resources/META-INF/rewrite/recipes.csv
@@ -305,6 +305,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.j
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javax.openJPAToEclipseLink,Migrate from OpenJPA to EclipseLink JPA,These recipes help migrate Java Persistence applications using OpenJPA to EclipseLink JPA.,10,,`javax` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.ExplicitRecordImport,Add explicit import for `Record` classes,"Add explicit import for `Record` classes when upgrading past Java 14+, to avoid conflicts with `java.lang.Record`.",1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.IfElseIfConstructToSwitch,If-else-if-else to switch,"Replace if-else-if-else with switch statements. In order to be replaced with a switch, all conditions must be on the same variable and there must be at least three cases.",1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
+maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.JavadocToMarkdownDocComment,Convert Javadoc to Markdown documentation comments,"Convert traditional Javadoc comments (`/** ... */`) to Markdown documentation comments (`///`) as supported by JEP 467 in Java 23+. Transforms HTML constructs like ``, ``, ``, ``, and lists to their Markdown equivalents, and converts inline tags like `{@code}` and `{@link}` to Markdown syntax.",1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.MigrateClassLoaderDefineClass,"Use `ClassLoader#defineClass(String, byte[], int, int)`","Use `ClassLoader#defineClass(String, byte[], int, int)` instead of the deprecated `ClassLoader#defineClass(byte[], int, int)` in Java 1.1 or higher.",1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.MigrateClassNewInstanceToGetDeclaredConstructorNewInstance,Use `Class#getDeclaredConstructor().newInstance()`,Use `Class#getDeclaredConstructor().newInstance()` instead of the deprecated `Class#newInstance()` in Java 9 or higher.,1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.lang.MigrateMainMethodToInstanceMain,Migrate `public static void main(String[] args)` to instance `void main()`,"Migrate `public static void main(String[] args)` method to instance `void main()` method when the `args` parameter is unused, as supported by JEP 512 in Java 25+.",1,,`java.lang` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
diff --git a/src/test/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocCommentTest.java b/src/test/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocCommentTest.java
new file mode 100644
index 0000000000..ca5e8b444d
--- /dev/null
+++ b/src/test/java/org/openrewrite/java/migrate/lang/JavadocToMarkdownDocCommentTest.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.java.migrate.lang;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+import static org.openrewrite.java.Assertions.version;
+
+class JavadocToMarkdownDocCommentTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new JavadocToMarkdownDocComment());
+ spec.allSources(source -> version(source, 23));
+ }
+
+ @DocumentExample
+ @Test
+ void multiLineJavadocWithTags() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Computes the sum of two numbers.
+ *
+ * @param a the first number
+ * @param b the second number
+ * @return the sum of a and b
+ */
+ public int add(int a, int b) {
+ return a + b;
+ }
+ }
+ """,
+ """
+ public class A {
+ /// Computes the sum of two numbers.
+ ///
+ /// @param a the first number
+ /// @param b the second number
+ /// @return the sum of a and b
+ public int add(int a, int b) {
+ return a + b;
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void singleLineJavadoc() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /** Returns the name. */
+ public String getName() {
+ return "name";
+ }
+ }
+ """,
+ """
+ public class A {
+ /// Returns the name.
+ public String getName() {
+ return "name";
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithCodeTag() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Use the {@code List} interface for ordered collections.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Use the `List` interface for ordered collections.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithLinkTag() {
+ rewriteRun(
+ java(
+ """
+ import java.util.List;
+ public class A {
+ /**
+ * Returns a {@link List} of items.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ import java.util.List;
+ public class A {
+ /// Returns a [List] of items.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithParagraph() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * First paragraph.
+ *
+ *
Second paragraph.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// First paragraph.
+ ///
+ /// Second paragraph.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithEmphasis() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * This is important text.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// This is _important_ text.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithStrong() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * This is very important text.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// This is **very important** text.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocOnClass() {
+ rewriteRun(
+ java(
+ """
+ /**
+ * A simple class.
+ *
+ * @since 1.0
+ */
+ public class A {
+ }
+ """,
+ """
+ /// A simple class.
+ ///
+ /// @since 1.0
+ public class A {
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithThrows() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Does something.
+ *
+ * @throws IllegalArgumentException if argument is invalid
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Does something.
+ ///
+ /// @throws IllegalArgumentException if argument is invalid
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithHtmlEntities() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Checks if a < b && c > d.
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Checks if a < b && c > d.
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithInheritDoc() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * {@inheritDoc}
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// {@inheritDoc}
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noChangeForRegularComments() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ // line comment
+ /* block comment */
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocOnField() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * The name of the entity.
+ */
+ private String name;
+ }
+ """,
+ """
+ public class A {
+ /// The name of the entity.
+ private String name;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithPreBlock() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Example usage:
+ *
+ * List<String> items = getItems();
+ * items.forEach(System.out::println);
+ *
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Example usage:
+ /// ```
+ /// List items = getItems();
+ /// items.forEach(System.out::println);
+ /// ```
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithUnorderedList() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Features:
+ *
+ * - Fast
+ * - Reliable
+ *
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Features:
+ ///
+ /// - Fast
+ /// - Reliable
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithDeprecated() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * @deprecated Use {@link #newMethod()} instead.
+ */
+ public void m() {}
+ public void newMethod() {}
+ }
+ """,
+ """
+ public class A {
+ /// @deprecated Use [#newMethod()] instead.
+ public void m() {}
+ public void newMethod() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocWithSee() {
+ rewriteRun(
+ java(
+ """
+ public class A {
+ /**
+ * Does something.
+ *
+ * @see Object#toString()
+ */
+ public void m() {}
+ }
+ """,
+ """
+ public class A {
+ /// Does something.
+ ///
+ /// @see Object#toString()
+ public void m() {}
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void javadocPreservesIndentationInNestedClass() {
+ rewriteRun(
+ java(
+ """
+ public class Outer {
+ public static class Inner {
+ /**
+ * Inner method.
+ */
+ public void m() {}
+ }
+ }
+ """,
+ """
+ public class Outer {
+ public static class Inner {
+ /// Inner method.
+ public void m() {}
+ }
+ }
+ """
+ )
+ );
+ }
+}