Skip to content

Commit 03fe076

Browse files
eamonnmcmanusgoogle-java-format Team
authored andcommitted
Only identify a /// comment as Markdown Javadoc if it is in a position where that makes sense.
The formatter currently recognizes and formats `/**` as Javadoc regardless of where it occurs. We could easily make it reformat only true Javadoc in that case too, by generalizing the logic here slightly. But the main motivation for the change here is that there may be a lot of "accidental Javadoc", since `///` comments only acquired their new meaning in Java 24. PiperOrigin-RevId: 891072159
1 parent 9eaf077 commit 03fe076

File tree

4 files changed

+125
-52
lines changed

4 files changed

+125
-52
lines changed

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: 14 additions & 12 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
@@ -56,7 +62,7 @@ public String rewrite(Tok tok, int maxWidth, int column0) {
5662
}
5763
}
5864
if (tok.isSlashSlashComment()) {
59-
return indentLineComments(lines, column0);
65+
return indentLineComments(tok, lines, column0);
6066
}
6167
return CommentsHelper.reformatParameterComment(tok)
6268
.orElseGet(
@@ -97,8 +103,8 @@ private String preserveIndentation(List<String> lines, int column0) {
97103
}
98104

99105
// Wraps and re-indents line comments.
100-
private String indentLineComments(List<String> lines, int column0) {
101-
lines = wrapLineComments(lines, column0);
106+
private String indentLineComments(Tok tok, List<String> lines, int column0) {
107+
lines = wrapLineComments(tok, lines, column0);
102108
StringBuilder builder = new StringBuilder();
103109
builder.append(lines.get(0).trim());
104110
String indentString = Strings.repeat(" ", column0);
@@ -108,21 +114,17 @@ private String indentLineComments(List<String> lines, int column0) {
108114
return builder.toString();
109115
}
110116

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

119-
private List<String> wrapLineComments(List<String> lines, int column0) {
122+
private List<String> wrapLineComments(Tok tok, List<String> lines, int column0) {
120123
List<String> result = new ArrayList<>();
121124
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.
125+
if (markdownJavadocPositions.contains(tok.getPosition())) {
126+
// Don't wrap markdown comments. Eventually we will format them properly, but for now at
127+
// least don't mangle them by wrapping with `// ` on the continuation lines.
126128
result.add(line);
127129
continue;
128130
}

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

Lines changed: 52 additions & 9 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,39 @@ && 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;
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. Currently, if the javadoc comment is a Markdown
3975+
// comment, each `///` line is its own token. So we need to continue searching backwards to find
3976+
// the first `///` in the sequence.
3977+
for (int i = toksBefore.size() - 1; i >= 0; i--) {
3978+
Input.Tok tok = toksBefore.get(i);
3979+
String text = tok.getText();
3980+
if (text.startsWith("/**")) {
3981+
return OptionalInt.of(tok.getPosition());
3982+
}
3983+
if (text.startsWith("///")) {
3984+
int j;
3985+
for (j = i - 1; j >= 0; j--) {
3986+
if (!toksBefore.get(j).getText().startsWith("///")) {
3987+
break;
3988+
}
39523989
}
3990+
return OptionalInt.of(toksBefore.get(j + 1).getPosition());
39533991
}
39543992
}
3955-
return false;
3993+
return OptionalInt.empty();
3994+
}
3995+
3996+
/** Does this declaration have javadoc preceding it? */
3997+
private boolean hasJavaDoc(Tree bodyDeclaration) {
3998+
return javadocPosition(bodyDeclaration).isPresent();
39563999
}
39574000

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

core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,37 +1600,53 @@ public void setSomething() {}
16001600
@Test
16011601
public void simpleMarkdown() {
16021602
String input =
1603-
"""
1604-
package com.example;
1605-
1606-
/// # Heading
1607-
///
1608-
/// A very long line of text, long enough that it will need to be wrapped to fit within the maximum line length.
1609-
class Test {
1610-
/// Another very long line of text, also long enough that it will need to be wrapped to fit within the maximum line length.
1611-
/// @param <T> a generic type
1612-
<T> T method() {
1613-
return null;
1614-
}
1615-
}\
1616-
""";
1603+
"""
1604+
package com.example;
1605+
1606+
/// # Heading
1607+
///
1608+
/// A very long line of text, long enough that it will need to be wrapped to fit within the maximum line length.
1609+
class Test {
1610+
/// Another very long line of text, also long enough that it will need to be wrapped to fit within the maximum line length.
1611+
/// @param <T> a generic type
1612+
<T> T method() {
1613+
return null;
1614+
}
1615+
1616+
/// This long line of text looks like a javadoc comment, but is not, because it is separated from the actual javadoc comment by a plain comment.
1617+
// This is the plain comment.
1618+
/// A third very long line of text, this time a javadoc comment on a field, which again exceeds the maximum line length.
1619+
String field;
1620+
1621+
/// A fourth very long line of text, which however is not a javadoc comment so will be wrapped like a regular // comment.
1622+
}\
1623+
""";
16171624
// TODO(emcmanus): Actually format the javadoc. For now, we just leave `///` lines alone, unlike
16181625
// `//` lines which get wrapped.
16191626
String expected =
1620-
"""
1621-
package com.example;
1622-
1623-
/// # Heading
1624-
///
1625-
/// A very long line of text, long enough that it will need to be wrapped to fit within the maximum line length.
1626-
class Test {
1627-
/// Another very long line of text, also long enough that it will need to be wrapped to fit within the maximum line length.
1628-
/// @param <T> a generic type
1629-
<T> T method() {
1630-
return null;
1631-
}
1632-
}
1633-
""";
1627+
"""
1628+
package com.example;
1629+
1630+
/// # Heading
1631+
///
1632+
/// A very long line of text, long enough that it will need to be wrapped to fit within the maximum line length.
1633+
class Test {
1634+
/// Another very long line of text, also long enough that it will need to be wrapped to fit within the maximum line length.
1635+
/// @param <T> a generic type
1636+
<T> T method() {
1637+
return null;
1638+
}
1639+
1640+
/// This long line of text looks like a javadoc comment, but is not, because it is separated from
1641+
// the actual javadoc comment by a plain comment.
1642+
// This is the plain comment.
1643+
/// A third very long line of text, this time a javadoc comment on a field, which again exceeds the maximum line length.
1644+
String field;
1645+
1646+
/// A fourth very long line of text, which however is not a javadoc comment so will be wrapped
1647+
// like a regular // comment.
1648+
}
1649+
""";
16341650
doFormatTest(input, expected);
16351651
}
16361652
}

0 commit comments

Comments
 (0)