Skip to content

Commit 72ddaac

Browse files
eamonnmcmanusgoogle-java-format Team
authored andcommitted
Initial support for Markdown Javadoc.
This is very incomplete and *will mangle some Markdown Javadoc comments*. Later changes will address that. PiperOrigin-RevId: 892607809
1 parent 9eaf077 commit 72ddaac

File tree

13 files changed

+473
-118
lines changed

13 files changed

+473
-118
lines changed

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
<groupId>com.google.guava</groupId>
4040
<artifactId>guava</artifactId>
4141
</dependency>
42+
<dependency>
43+
<groupId>org.commonmark</groupId>
44+
<artifactId>commonmark-parent</artifactId>
45+
<version>0.28.0</version>
46+
</dependency>
4247

4348
<!-- Compile-time dependencies -->
4449
<dependency>

core/src/main/java/com/google/googlejavaformat/java/Formatter.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717

1818
import com.google.common.collect.ImmutableList;
19+
import com.google.common.collect.ImmutableSet;
1920
import com.google.common.collect.Iterators;
2021
import com.google.common.collect.Range;
2122
import com.google.common.collect.RangeSet;
2223
import com.google.common.collect.TreeRangeSet;
2324
import com.google.common.io.CharSink;
2425
import com.google.common.io.CharSource;
2526
import com.google.errorprone.annotations.Immutable;
27+
import com.google.googlejavaformat.CommentsHelper;
2628
import com.google.googlejavaformat.Doc;
2729
import com.google.googlejavaformat.DocBuilder;
2830
import com.google.googlejavaformat.FormattingError;
@@ -109,13 +111,20 @@ static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatt
109111
throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
110112
}
111113
OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
114+
ImmutableSet.Builder<Integer> markdownJavadocPositions = ImmutableSet.builder();
112115
// Output the compilation unit.
113-
JavaInputAstVisitor visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
116+
JavaInputAstVisitor visitor =
117+
new JavaInputAstVisitor(builder, options.indentationMultiplier(), markdownJavadocPositions);
114118
visitor.scan(unit, null);
115119
builder.sync(javaInput.getText().length());
116120
builder.drain();
117121
Doc doc = new DocBuilder().withOps(builder.build()).build();
118-
doc.computeBreaks(javaOutput.getCommentsHelper(), MAX_LINE_LENGTH, new Doc.State(+0, 0));
122+
CommentsHelper commentsHelper =
123+
new JavaCommentsHelper(
124+
Newlines.guessLineSeparator(javaInput.getText()),
125+
options,
126+
markdownJavadocPositions.build());
127+
doc.computeBreaks(commentsHelper, MAX_LINE_LENGTH, new Doc.State(+0, 0));
119128
doc.write(javaOutput);
120129
javaOutput.flush();
121130
}
@@ -209,7 +218,10 @@ public ImmutableList<Replacement> getFormatReplacements(
209218

210219
String lineSeparator = Newlines.guessLineSeparator(input);
211220
JavaOutput javaOutput =
212-
new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options));
221+
new JavaOutput(
222+
lineSeparator,
223+
javaInput,
224+
new JavaCommentsHelper(lineSeparator, options, ImmutableSet.of()));
213225
try {
214226
format(javaInput, javaOutput, options);
215227
} catch (FormattingError e) {

core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.google.common.base.CharMatcher;
1818
import com.google.common.base.Strings;
19+
import com.google.common.collect.ImmutableSet;
1920
import com.google.googlejavaformat.CommentsHelper;
2021
import com.google.googlejavaformat.Input.Tok;
2122
import com.google.googlejavaformat.Newlines;
@@ -31,10 +32,15 @@ final class JavaCommentsHelper implements CommentsHelper {
3132

3233
private final String lineSeparator;
3334
private final JavaFormatterOptions options;
35+
private final ImmutableSet<Integer> markdownJavadocPositions;
3436

35-
JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) {
37+
JavaCommentsHelper(
38+
String lineSeparator,
39+
JavaFormatterOptions options,
40+
ImmutableSet<Integer> markdownJavadocPositions) {
3641
this.lineSeparator = lineSeparator;
3742
this.options = options;
43+
this.markdownJavadocPositions = markdownJavadocPositions;
3844
}
3945

4046
@Override
@@ -44,7 +50,13 @@ public String rewrite(Tok tok, int maxWidth, int column0) {
4450
}
4551
String text = tok.getOriginalText();
4652
if (tok.isJavadocComment() && options.formatJavadoc()) {
47-
text = JavadocFormatter.formatJavadoc(text, column0);
53+
if (text.startsWith("///")) {
54+
if (markdownJavadocPositions.contains(tok.getPosition())) {
55+
return JavadocFormatter.formatJavadoc(text, column0);
56+
}
57+
} else {
58+
text = JavadocFormatter.formatJavadoc(text, column0);
59+
}
4860
}
4961
List<String> lines = new ArrayList<>();
5062
Iterator<String> it = Newlines.lineIterator(text);
@@ -56,7 +68,7 @@ public String rewrite(Tok tok, int maxWidth, int column0) {
5668
}
5769
}
5870
if (tok.isSlashSlashComment()) {
59-
return indentLineComments(lines, column0);
71+
return indentLineComments(tok, lines, column0);
6072
}
6173
return CommentsHelper.reformatParameterComment(tok)
6274
.orElseGet(
@@ -97,8 +109,8 @@ private String preserveIndentation(List<String> lines, int column0) {
97109
}
98110

99111
// Wraps and re-indents line comments.
100-
private String indentLineComments(List<String> lines, int column0) {
101-
lines = wrapLineComments(lines, column0);
112+
private String indentLineComments(Tok tok, List<String> lines, int column0) {
113+
lines = wrapLineComments(tok, lines, column0);
102114
StringBuilder builder = new StringBuilder();
103115
builder.append(lines.get(0).trim());
104116
String indentString = Strings.repeat(" ", column0);
@@ -108,21 +120,17 @@ private String indentLineComments(List<String> lines, int column0) {
108120
return builder.toString();
109121
}
110122

111-
/** Probably a markdown comment, so don't try to wrap it. */
112-
private static final Pattern MARKDOWN_JAVADOC_PREFIX = Pattern.compile("^///(\\s|$)");
113-
114123
// Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
115124
// contain leading spaces.
116125
private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX =
117126
Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]");
118127

119-
private List<String> wrapLineComments(List<String> lines, int column0) {
128+
private List<String> wrapLineComments(Tok tok, List<String> lines, int column0) {
120129
List<String> result = new ArrayList<>();
121130
for (String line : lines) {
122-
if (MARKDOWN_JAVADOC_PREFIX.matcher(line).find()) {
123-
// Don't try to wrap comments that might be markdown javadoc.
124-
// This is fairly approximate: a /// comment is only javadoc if it precedes a javadocable
125-
// program element. But even if this isn't javadoc, it's not a disaster if we don't wrap it.
131+
if (markdownJavadocPositions.contains(tok.getPosition())) {
132+
// Don't wrap markdown comments. Eventually we will format them properly, but for now at
133+
// least don't mangle them by wrapping with `// ` on the continuation lines.
126134
result.add(line);
127135
continue;
128136
}

core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,13 @@ public boolean isSlashStarComment() {
160160

161161
@Override
162162
public boolean isJavadocComment() {
163-
// comments like `/***` are also javadoc, but their formatting probably won't be improved
164-
// by the javadoc formatter
165-
return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4;
163+
// comments like `/***` or `////` are also javadoc, but their formatting probably won't be
164+
// improved by the javadoc formatter
165+
return ((text.startsWith("/**") && !text.startsWith("/***"))
166+
|| (Runtime.version().feature() >= 23
167+
&& text.startsWith("///")
168+
&& !text.startsWith("////")))
169+
&& text.length() > 4;
166170
}
167171

168172
@Override

core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
import java.util.List;
171171
import java.util.Map;
172172
import java.util.Optional;
173+
import java.util.OptionalInt;
173174
import java.util.Set;
174175
import java.util.regex.Pattern;
175176
import java.util.stream.Stream;
@@ -270,6 +271,7 @@ private static ImmutableSetMultimap<String, String> typeAnnotations() {
270271
}
271272

272273
private final OpsBuilder builder;
274+
private final ImmutableSet.Builder<Integer> markdownJavadocPositions;
273275

274276
private static final Indent.Const ZERO = Indent.Const.ZERO;
275277
private final int indentMultiplier;
@@ -306,9 +308,13 @@ private static final ImmutableList<Op> forceBreakList(Optional<BreakTag> breakTa
306308
*
307309
* @param builder the {@link OpsBuilder}
308310
*/
309-
JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) {
311+
JavaInputAstVisitor(
312+
OpsBuilder builder,
313+
int indentMultiplier,
314+
ImmutableSet.Builder<Integer> markdownJavadocPositions) {
310315
this.builder = builder;
311316
this.indentMultiplier = indentMultiplier;
317+
this.markdownJavadocPositions = markdownJavadocPositions;
312318
minusTwo = Indent.Const.make(-2, indentMultiplier);
313319
minusFour = Indent.Const.make(-4, indentMultiplier);
314320
plusTwo = Indent.Const.make(+2, indentMultiplier);
@@ -396,7 +402,7 @@ private void handleModule(boolean afterFirstToken, CompilationUnitTree node) {
396402
}
397403
}
398404

399-
/** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */
405+
/** Skips over extra semicolons at the top-level, or in a class member declaration lists. */
400406
private void dropEmptyDeclarations() {
401407
if (builder.peekToken().equals(Optional.of(";"))) {
402408
while (builder.peekToken().equals(Optional.of(";"))) {
@@ -407,6 +413,16 @@ private void dropEmptyDeclarations() {
407413
}
408414
}
409415

416+
private void recordMarkdownJavadocPosition(Tree tree) {
417+
OptionalInt javadocPosition = javadocPosition(tree);
418+
if (javadocPosition.isPresent()) {
419+
int pos = javadocPosition.getAsInt();
420+
if (builder.getInput().getText().startsWith("///", pos)) {
421+
markdownJavadocPositions.add(pos);
422+
}
423+
}
424+
}
425+
410426
// Replace with Flags.IMPLICIT_CLASS once JDK 25 is the minimum supported version
411427
private static final int IMPLICIT_CLASS = 1 << 19;
412428

@@ -416,6 +432,7 @@ public Void visitClass(ClassTree tree, Void unused) {
416432
visitImplicitClass(tree);
417433
return null;
418434
}
435+
recordMarkdownJavadocPosition(tree);
419436
switch (tree.getKind()) {
420437
case ANNOTATION_TYPE -> visitAnnotationType(tree);
421438
case CLASS, INTERFACE -> visitClassDeclaration(tree);
@@ -1020,6 +1037,9 @@ private void visitVariables(
10201037
Direction annotationDirection) {
10211038
if (fragments.size() == 1) {
10221039
VariableTree fragment = fragments.get(0);
1040+
if (declarationKind == DeclarationKind.FIELD) {
1041+
recordMarkdownJavadocPosition(fragment);
1042+
}
10231043
declareOne(
10241044
declarationKind,
10251045
annotationDirection,
@@ -1455,6 +1475,7 @@ public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) {
14551475

14561476
@Override
14571477
public Void visitMethod(MethodTree node, Void unused) {
1478+
recordMarkdownJavadocPosition(node);
14581479
sync(node);
14591480
List<? extends AnnotationTree> annotations = node.getModifiers().getAnnotations();
14601481
List<? extends AnnotationTree> returnTypeAnnotations = ImmutableList.of();
@@ -2781,6 +2802,7 @@ public Void visitIdentifier(IdentifierTree node, Void unused) {
27812802

27822803
@Override
27832804
public Void visitModule(ModuleTree node, Void unused) {
2805+
recordMarkdownJavadocPosition(node);
27842806
for (AnnotationTree annotation : node.getAnnotations()) {
27852807
scan(annotation, null);
27862808
builder.forcedBreak();
@@ -3941,18 +3963,34 @@ && getStartPosition(it.peek()) == start) {
39413963
return fragments;
39423964
}
39433965

3944-
/** Does this declaration have javadoc preceding it? */
3945-
private boolean hasJavaDoc(Tree bodyDeclaration) {
3966+
private OptionalInt javadocPosition(Tree bodyDeclaration) {
39463967
int position = ((JCTree) bodyDeclaration).getStartPosition();
39473968
Input.Token token = builder.getInput().getPositionTokenMap().get(position);
3948-
if (token != null) {
3949-
for (Input.Tok tok : token.getToksBefore()) {
3950-
if (tok.getText().startsWith("/**")) {
3951-
return true;
3952-
}
3969+
if (token == null) {
3970+
return OptionalInt.empty();
3971+
}
3972+
var toksBefore = token.getToksBefore();
3973+
// toksBefore is in source order. If there are several comments preceding bodyDeclaration, we
3974+
// want the last one that is a javadoc comment.
3975+
for (int i = toksBefore.size() - 1; i >= 0; i--) {
3976+
Input.Tok tok = toksBefore.get(i);
3977+
String text = tok.getText();
3978+
// TODO: consider making earlier versions behave compatibly. Prior to Java 23, there are no
3979+
// markdown javadoc comments, and /// is just a regular // comment that happens to start with
3980+
// an additional slash. As of Java 23, /// is markdown javadoc, and all consecutive /// lines
3981+
// are part of the same javac token. To ensure consistent formatting before and after this
3982+
// change, we would have to merge /// lines in a compatible way, including when we are
3983+
// extracting their text to make a comment that might be reformatted.
3984+
if (text.startsWith("/**") || (Runtime.version().feature() >= 23 && text.startsWith("///"))) {
3985+
return OptionalInt.of(tok.getPosition());
39533986
}
39543987
}
3955-
return false;
3988+
return OptionalInt.empty();
3989+
}
3990+
3991+
/** Does this declaration have javadoc preceding it? */
3992+
private boolean hasJavaDoc(Tree bodyDeclaration) {
3993+
return javadocPosition(bodyDeclaration).isPresent();
39563994
}
39573995

39583996
private static Optional<? extends Input.Token> getNextToken(Input input, int position) {

core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@
5858
import org.jspecify.annotations.Nullable;
5959

6060
/**
61-
* Removes unused imports from a source file. Imports that are only used in javadoc are also
62-
* removed, and the references in javadoc are replaced with fully qualified names.
61+
* Removes unused imports from a source file. Imports are preserved even if they are only used in
62+
* javadoc links.
6363
*/
6464
public class RemoveUnusedImports {
6565

core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,18 @@
2727
*/
2828
final class CharStream {
2929
private final String input;
30-
private int start;
30+
private int position;
3131
private int tokenEnd = -1; // Negative value means no token, and will cause an exception if used.
3232

3333
CharStream(String input) {
3434
this.input = checkNotNull(input);
3535
}
3636

3737
boolean tryConsume(String expected) {
38-
if (!input.startsWith(expected, start)) {
38+
if (!input.startsWith(expected, position)) {
3939
return false;
4040
}
41-
tokenEnd = start + expected.length();
41+
tokenEnd = position + expected.length();
4242
return true;
4343
}
4444

@@ -48,7 +48,7 @@ boolean tryConsume(String expected) {
4848
* @param pattern the pattern to search for, which must be anchored to match only at position 0
4949
*/
5050
boolean tryConsumeRegex(Pattern pattern) {
51-
Matcher matcher = pattern.matcher(input).region(start, input.length());
51+
Matcher matcher = pattern.matcher(input).region(position, input.length());
5252
if (!matcher.lookingAt()) {
5353
return false;
5454
}
@@ -57,13 +57,21 @@ boolean tryConsumeRegex(Pattern pattern) {
5757
}
5858

5959
String readAndResetRecorded() {
60-
String result = input.substring(start, tokenEnd);
61-
start = tokenEnd;
60+
String result = input.substring(position, tokenEnd);
61+
position = tokenEnd;
6262
tokenEnd = -1;
6363
return result;
6464
}
6565

6666
boolean isExhausted() {
67-
return start == input.length();
67+
return position == input.length();
68+
}
69+
70+
int position() {
71+
return position;
72+
}
73+
74+
String substring(int start, int end) {
75+
return input.substring(start, end);
6876
}
6977
}

0 commit comments

Comments
 (0)