Skip to content

Commit 43874cd

Browse files
cushonError Prone Team
authored andcommitted
Implement MissingJavadoc Error Prone check
Add a consolidated MissingJavadoc Error Prone check that flags missing Javadoc comments on public classes, interfaces, enums, annotations, and public/protected methods and fields. Also includes a special case for nested Builder classes to suggest a tailored diagnostic and prefix SuggestedFix. PiperOrigin-RevId: 940399041
1 parent a9851fe commit 43874cd

3 files changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2026 The Error Prone Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.errorprone.bugpatterns.javadoc;
18+
19+
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
20+
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
21+
import static com.google.errorprone.matchers.Description.NO_MATCH;
22+
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
23+
import static com.google.errorprone.util.ASTHelpers.findSuperMethods;
24+
import static com.google.errorprone.util.ASTHelpers.getSymbol;
25+
import static com.google.errorprone.util.ASTHelpers.isEffectivelyPrivate;
26+
27+
import com.google.common.collect.ImmutableSet;
28+
import com.google.errorprone.BugPattern;
29+
import com.google.errorprone.VisitorState;
30+
import com.google.errorprone.bugpatterns.BugChecker;
31+
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
32+
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
33+
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
34+
import com.google.errorprone.fixes.SuggestedFix;
35+
import com.google.errorprone.matchers.Description;
36+
import com.sun.source.doctree.DocCommentTree;
37+
import com.sun.source.tree.AssignmentTree;
38+
import com.sun.source.tree.ClassTree;
39+
import com.sun.source.tree.ExpressionStatementTree;
40+
import com.sun.source.tree.MethodTree;
41+
import com.sun.source.tree.ReturnTree;
42+
import com.sun.source.tree.StatementTree;
43+
import com.sun.source.tree.Tree;
44+
import com.sun.source.tree.VariableTree;
45+
import com.sun.tools.javac.api.JavacTrees;
46+
import com.sun.tools.javac.code.Symbol;
47+
import com.sun.tools.javac.code.Symbol.ClassSymbol;
48+
import com.sun.tools.javac.code.Symbol.MethodSymbol;
49+
import java.util.Collections;
50+
import java.util.List;
51+
import javax.lang.model.element.ElementKind;
52+
import javax.lang.model.element.Modifier;
53+
54+
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
55+
@BugPattern(
56+
summary = "Public types and public/protected members must have Javadoc comments.",
57+
severity = WARNING,
58+
linkType = CUSTOM,
59+
link = "https://google.github.io/styleguide/javaguide.html#s7.1.1-where-required",
60+
documentSuppression = false)
61+
public final class MissingJavadoc extends BugChecker
62+
implements ClassTreeMatcher, MethodTreeMatcher, VariableTreeMatcher {
63+
64+
@Override
65+
public Description matchClass(ClassTree classTree, VisitorState state) {
66+
return checkJavadoc(classTree, getSymbol(classTree), state);
67+
}
68+
69+
@Override
70+
public Description matchMethod(MethodTree methodTree, VisitorState state) {
71+
MethodSymbol symbol = getSymbol(methodTree);
72+
if (symbol == null) {
73+
return NO_MATCH;
74+
}
75+
if (!findSuperMethods(symbol, state.getTypes()).isEmpty()) {
76+
return NO_MATCH;
77+
}
78+
if (isSimpleGetterOrSetter(methodTree)) {
79+
return NO_MATCH;
80+
}
81+
return checkJavadoc(methodTree, symbol, state);
82+
}
83+
84+
@Override
85+
public Description matchVariable(VariableTree variableTree, VisitorState state) {
86+
Symbol symbol = getSymbol(variableTree);
87+
if (symbol == null || symbol.getKind() != ElementKind.FIELD) {
88+
return NO_MATCH;
89+
}
90+
return checkJavadoc(variableTree, symbol, state);
91+
}
92+
93+
private Description checkJavadoc(Tree tree, Symbol symbol, VisitorState state) {
94+
if (state.errorProneOptions().isTestOnlyTarget()) {
95+
return NO_MATCH;
96+
}
97+
if (isEffectivelyPrivate(symbol)) {
98+
return NO_MATCH;
99+
}
100+
if (!isEffectivelyPublicOrProtected(symbol)) {
101+
return NO_MATCH;
102+
}
103+
DocCommentTree docCommentTree =
104+
JavacTrees.instance(state.context).getDocCommentTree(state.getPath());
105+
if (docCommentTree != null) {
106+
return NO_MATCH;
107+
}
108+
Description.Builder description = buildDescription(tree);
109+
if (tree instanceof ClassTree classTree
110+
&& classTree.getSimpleName().toString().endsWith("Builder")) {
111+
ClassSymbol enclosing = enclosingClass(symbol);
112+
if (enclosing != null) {
113+
String suggestedJavadoc =
114+
String.format("/** A builder for {@link %s}. */\n", enclosing.getSimpleName());
115+
description
116+
.setMessage(
117+
"Builder classes require Javadoc comments. Consider adding: %s",
118+
suggestedJavadoc.trim())
119+
.addFix(SuggestedFix.prefixWith(tree, suggestedJavadoc));
120+
}
121+
}
122+
return description.build();
123+
}
124+
125+
private static boolean isSimpleGetterOrSetter(MethodTree methodTree) {
126+
if (methodTree.getBody() == null) {
127+
return false;
128+
}
129+
List<? extends StatementTree> statements = methodTree.getBody().getStatements();
130+
if (statements.size() != 1) {
131+
return false;
132+
}
133+
StatementTree stmt = statements.get(0);
134+
return switch (stmt) {
135+
case ReturnTree returnTree -> true;
136+
case ExpressionStatementTree expressionStatementTree ->
137+
expressionStatementTree.getExpression() instanceof AssignmentTree;
138+
default -> false;
139+
};
140+
}
141+
142+
private static final ImmutableSet<Modifier> PUBLIC_OR_PROTECTED =
143+
ImmutableSet.of(Modifier.PUBLIC, Modifier.PROTECTED);
144+
145+
private static boolean isEffectivelyPublicOrProtected(Symbol symbol) {
146+
Symbol current = symbol;
147+
while (current != null) {
148+
if (current.getKind() == ElementKind.PACKAGE) {
149+
break;
150+
}
151+
if (Collections.disjoint(current.getModifiers(), PUBLIC_OR_PROTECTED)) {
152+
return false;
153+
}
154+
current = current.owner;
155+
}
156+
return true;
157+
}
158+
}

core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@
587587
import com.google.errorprone.bugpatterns.javadoc.InvalidThrows;
588588
import com.google.errorprone.bugpatterns.javadoc.InvalidThrowsLink;
589589
import com.google.errorprone.bugpatterns.javadoc.MalformedInlineTag;
590+
import com.google.errorprone.bugpatterns.javadoc.MissingJavadoc;
590591
import com.google.errorprone.bugpatterns.javadoc.MissingSummary;
591592
import com.google.errorprone.bugpatterns.javadoc.NotJavadoc;
592593
import com.google.errorprone.bugpatterns.javadoc.PreferThrowsTag;
@@ -1297,6 +1298,7 @@ public static ScannerSupplier warningChecks() {
12971298
MethodCanBeStatic.class,
12981299
MissingBraces.class,
12991300
MissingDefault.class,
1301+
MissingJavadoc.class,
13001302
MissingRuntimeRetention.class,
13011303
MixedArrayDimensions.class,
13021304
MockitoDoSetup.class,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2026 The Error Prone Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.errorprone.bugpatterns.javadoc;
18+
19+
import com.google.errorprone.BugCheckerRefactoringTestHelper;
20+
import com.google.errorprone.CompilationTestHelper;
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
import org.junit.runners.JUnit4;
24+
25+
/** Unit tests for {@link MissingJavadoc} bug pattern. */
26+
@RunWith(JUnit4.class)
27+
public final class MissingJavadocTest {
28+
private final CompilationTestHelper compilationHelper =
29+
CompilationTestHelper.newInstance(MissingJavadoc.class, getClass());
30+
private final BugCheckerRefactoringTestHelper refactoringHelper =
31+
BugCheckerRefactoringTestHelper.newInstance(MissingJavadoc.class, getClass());
32+
33+
@Test
34+
public void publicClassWithoutJavadoc_warns() {
35+
compilationHelper
36+
.addSourceLines(
37+
"Test.java",
38+
"""
39+
// BUG: Diagnostic contains: MissingJavadoc
40+
public class Test {}
41+
""")
42+
.doTest();
43+
}
44+
45+
@Test
46+
public void publicClassWithJavadoc_passes() {
47+
compilationHelper
48+
.addSourceLines(
49+
"Test.java",
50+
"""
51+
/** This is a class doc. */
52+
public class Test {}
53+
""")
54+
.doTest();
55+
}
56+
57+
@Test
58+
public void privateClassWithoutJavadoc_passes() {
59+
compilationHelper
60+
.addSourceLines(
61+
"Test.java",
62+
"""
63+
/** Class doc. */
64+
public class Test {
65+
private static class Inner {}
66+
}
67+
""")
68+
.doTest();
69+
}
70+
71+
@Test
72+
public void publicMethodWithoutJavadoc_warns() {
73+
compilationHelper
74+
.addSourceLines(
75+
"Test.java",
76+
"""
77+
/** This is class doc. */
78+
public class Test {
79+
// BUG: Diagnostic contains: MissingJavadoc
80+
public void foo() {}
81+
}
82+
""")
83+
.doTest();
84+
}
85+
86+
@Test
87+
public void publicMethodWithJavadoc_passes() {
88+
compilationHelper
89+
.addSourceLines(
90+
"Test.java",
91+
"""
92+
/** This is class doc. */
93+
public class Test {
94+
/** Method doc. */
95+
public void foo() {}
96+
}
97+
""")
98+
.doTest();
99+
}
100+
101+
@Test
102+
public void simpleGetterAndSetter_passes() {
103+
compilationHelper
104+
.addSourceLines(
105+
"Test.java",
106+
"""
107+
/** This is class doc. */
108+
public class Test {
109+
private int x;
110+
111+
public int getX() {
112+
return x;
113+
}
114+
115+
public void setX(int x) {
116+
this.x = x;
117+
}
118+
}
119+
""")
120+
.doTest();
121+
}
122+
123+
@Test
124+
public void overriddenMethodWithoutJavadoc_passes() {
125+
compilationHelper
126+
.addSourceLines(
127+
"Test.java",
128+
"""
129+
/** This is class doc. */
130+
public class Test implements Runnable {
131+
@Override
132+
public void run() {}
133+
}
134+
""")
135+
.doTest();
136+
}
137+
138+
@Test
139+
public void builderClassSuggestedFix() {
140+
refactoringHelper
141+
.addInputLines(
142+
"Test.java",
143+
"""
144+
/** This is class doc. */
145+
public class Test {
146+
public static final class Builder {}
147+
}
148+
""")
149+
.addOutputLines(
150+
"Test.java",
151+
"""
152+
/** This is class doc. */
153+
public class Test {
154+
/** A builder for {@link Test}. */
155+
public static final class Builder {}
156+
}
157+
""")
158+
.doTest();
159+
}
160+
161+
@Test
162+
public void privateBuilderClass_noSuggestedFix() {
163+
compilationHelper
164+
.addSourceLines(
165+
"Test.java",
166+
"""
167+
/** This is class doc. */
168+
public class Test {
169+
private static final class Builder {}
170+
}
171+
""")
172+
.doTest();
173+
}
174+
}

0 commit comments

Comments
 (0)