diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadoc.java b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadoc.java new file mode 100644 index 00000000000..da9e98ee848 --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadoc.java @@ -0,0 +1,158 @@ +/* + * Copyright 2026 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.google.errorprone.bugpatterns.javadoc; + +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.enclosingClass; +import static com.google.errorprone.util.ASTHelpers.findSuperMethods; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.isEffectivelyPrivate; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.api.JavacTrees; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import java.util.Collections; +import java.util.List; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; + +/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */ +@BugPattern( + summary = "Public types and public/protected members must have Javadoc comments.", + severity = WARNING, + linkType = CUSTOM, + link = "https://google.github.io/styleguide/javaguide.html#s7.1.1-where-required", + documentSuppression = false) +public final class MissingJavadoc extends BugChecker + implements ClassTreeMatcher, MethodTreeMatcher, VariableTreeMatcher { + + @Override + public Description matchClass(ClassTree classTree, VisitorState state) { + return checkJavadoc(classTree, getSymbol(classTree), state); + } + + @Override + public Description matchMethod(MethodTree methodTree, VisitorState state) { + MethodSymbol symbol = getSymbol(methodTree); + if (symbol == null) { + return NO_MATCH; + } + if (!findSuperMethods(symbol, state.getTypes()).isEmpty()) { + return NO_MATCH; + } + if (isSimpleGetterOrSetter(methodTree)) { + return NO_MATCH; + } + return checkJavadoc(methodTree, symbol, state); + } + + @Override + public Description matchVariable(VariableTree variableTree, VisitorState state) { + Symbol symbol = getSymbol(variableTree); + if (symbol == null || symbol.getKind() != ElementKind.FIELD) { + return NO_MATCH; + } + return checkJavadoc(variableTree, symbol, state); + } + + private Description checkJavadoc(Tree tree, Symbol symbol, VisitorState state) { + if (state.errorProneOptions().isTestOnlyTarget()) { + return NO_MATCH; + } + if (isEffectivelyPrivate(symbol)) { + return NO_MATCH; + } + if (!isEffectivelyPublicOrProtected(symbol)) { + return NO_MATCH; + } + DocCommentTree docCommentTree = + JavacTrees.instance(state.context).getDocCommentTree(state.getPath()); + if (docCommentTree != null) { + return NO_MATCH; + } + Description.Builder description = buildDescription(tree); + if (tree instanceof ClassTree classTree + && classTree.getSimpleName().toString().endsWith("Builder")) { + ClassSymbol enclosing = enclosingClass(symbol); + if (enclosing != null) { + String suggestedJavadoc = + String.format("/** A builder for {@link %s}. */\n", enclosing.getSimpleName()); + description + .setMessage( + "Builder classes require Javadoc comments. Consider adding: %s", + suggestedJavadoc.trim()) + .addFix(SuggestedFix.prefixWith(tree, suggestedJavadoc)); + } + } + return description.build(); + } + + private static boolean isSimpleGetterOrSetter(MethodTree methodTree) { + if (methodTree.getBody() == null) { + return false; + } + List statements = methodTree.getBody().getStatements(); + if (statements.size() != 1) { + return false; + } + StatementTree stmt = statements.get(0); + return switch (stmt) { + case ReturnTree returnTree -> true; + case ExpressionStatementTree expressionStatementTree -> + expressionStatementTree.getExpression() instanceof AssignmentTree; + default -> false; + }; + } + + private static final ImmutableSet PUBLIC_OR_PROTECTED = + ImmutableSet.of(Modifier.PUBLIC, Modifier.PROTECTED); + + private static boolean isEffectivelyPublicOrProtected(Symbol symbol) { + Symbol current = symbol; + while (current != null) { + if (current.getKind() == ElementKind.PACKAGE) { + break; + } + if (Collections.disjoint(current.getModifiers(), PUBLIC_OR_PROTECTED)) { + return false; + } + current = current.owner; + } + return true; + } +} diff --git a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 39c9c55afbc..11417a8ef85 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -587,6 +587,7 @@ import com.google.errorprone.bugpatterns.javadoc.InvalidThrows; import com.google.errorprone.bugpatterns.javadoc.InvalidThrowsLink; import com.google.errorprone.bugpatterns.javadoc.MalformedInlineTag; +import com.google.errorprone.bugpatterns.javadoc.MissingJavadoc; import com.google.errorprone.bugpatterns.javadoc.MissingSummary; import com.google.errorprone.bugpatterns.javadoc.NotJavadoc; import com.google.errorprone.bugpatterns.javadoc.PreferThrowsTag; @@ -1297,6 +1298,7 @@ public static ScannerSupplier warningChecks() { MethodCanBeStatic.class, MissingBraces.class, MissingDefault.class, + MissingJavadoc.class, MissingRuntimeRetention.class, MixedArrayDimensions.class, MockitoDoSetup.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadocTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadocTest.java new file mode 100644 index 00000000000..11049c126d2 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/javadoc/MissingJavadocTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2026 The Error Prone Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.google.errorprone.bugpatterns.javadoc; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link MissingJavadoc} bug pattern. */ +@RunWith(JUnit4.class) +public final class MissingJavadocTest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(MissingJavadoc.class, getClass()); + private final BugCheckerRefactoringTestHelper refactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(MissingJavadoc.class, getClass()); + + @Test + public void publicClassWithoutJavadoc_warns() { + compilationHelper + .addSourceLines( + "Test.java", + """ + // BUG: Diagnostic contains: MissingJavadoc + public class Test {} + """) + .doTest(); + } + + @Test + public void publicClassWithJavadoc_passes() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is a class doc. */ + public class Test {} + """) + .doTest(); + } + + @Test + public void privateClassWithoutJavadoc_passes() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** Class doc. */ + public class Test { + private static class Inner {} + } + """) + .doTest(); + } + + @Test + public void publicMethodWithoutJavadoc_warns() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + // BUG: Diagnostic contains: MissingJavadoc + public void foo() {} + } + """) + .doTest(); + } + + @Test + public void publicMethodWithJavadoc_passes() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + /** Method doc. */ + public void foo() {} + } + """) + .doTest(); + } + + @Test + public void simpleGetterAndSetter_passes() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + private int x; + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + } + """) + .doTest(); + } + + @Test + public void overriddenMethodWithoutJavadoc_passes() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is class doc. */ + public class Test implements Runnable { + @Override + public void run() {} + } + """) + .doTest(); + } + + @Test + public void builderClassSuggestedFix() { + refactoringHelper + .addInputLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + public static final class Builder {} + } + """) + .addOutputLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + /** A builder for {@link Test}. */ + public static final class Builder {} + } + """) + .doTest(); + } + + @Test + public void privateBuilderClass_noSuggestedFix() { + compilationHelper + .addSourceLines( + "Test.java", + """ + /** This is class doc. */ + public class Test { + private static final class Builder {} + } + """) + .doTest(); + } +}