com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocStyleCheck Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of checkstyle Show documentation
Show all versions of checkstyle Show documentation
Checkstyle is a development tool to help programmers write Java code
that adheres to a coding standard
The newest version!
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.javadoc;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.Scope;
import com.puppycrawl.tools.checkstyle.api.TextBlock;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
/**
*
* Validates Javadoc comments to help ensure they are well formed.
*
*
*
* The following checks are performed:
*
*
* -
* Ensures the first sentence ends with proper punctuation
* (That is a period, question mark, or exclamation mark, by default).
* Note that this check is not applied to inline {@code @return} tags,
* because the Javadoc tools automatically appends a period to the end of the tag
* content.
* Javadoc automatically places the first sentence in the method summary
* table and index. Without proper punctuation the Javadoc may be malformed.
* All items eligible for the {@code {@inheritDoc}} tag are exempt from this
* requirement.
*
* -
* Check text for Javadoc statements that do not have any description.
* This includes both completely empty Javadoc, and Javadoc with only tags
* such as {@code @param} and {@code @return}.
*
* -
* Check text for incomplete HTML tags. Verifies that HTML tags have
* corresponding end tags and issues an "Unclosed HTML tag found:" error if not.
* An "Extra HTML tag found:" error is issued if an end tag is found without
* a previous open tag.
*
* -
* Check that a package Javadoc comment is well-formed (as described above).
*
* -
* Check for allowed HTML tags. The list of allowed HTML tags is
* "a", "abbr", "acronym", "address", "area", "b", "bdo", "big", "blockquote",
* "br", "caption", "cite", "code", "colgroup", "dd", "del", "dfn", "div", "dl",
* "dt", "em", "fieldset", "font", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
* "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "samp", "small",
* "span", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
* "thead", "tr", "tt", "u", "ul", "var".
*
*
*
*
* These checks were patterned after the checks made by the
* DocCheck doclet
* available from Sun. Note: Original Sun's DocCheck tool does not exist anymore.
*
*
* -
* Property {@code checkEmptyJavadoc} - Control whether to check if the Javadoc
* is missing a describing text.
* Type is {@code boolean}.
* Default value is {@code false}.
*
* -
* Property {@code checkFirstSentence} - Control whether to check the first
* sentence for proper end of sentence.
* Type is {@code boolean}.
* Default value is {@code true}.
*
* -
* Property {@code checkHtml} - Control whether to check for incomplete HTML tags.
* Type is {@code boolean}.
* Default value is {@code true}.
*
* -
* Property {@code endOfSentenceFormat} - Specify the format for matching
* the end of a sentence.
* Type is {@code java.util.regex.Pattern}.
* Default value is {@code "([.?!][ \t\n\r\f<])|([.?!]$)"}.
*
* -
* Property {@code excludeScope} - Specify the visibility scope where
* Javadoc comments are not checked.
* Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
* Default value is {@code null}.
*
* -
* Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
* Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
* Default value is {@code private}.
*
* -
* Property {@code tokens} - tokens to check
* Type is {@code java.lang.String[]}.
* Validation type is {@code tokenSet}.
* Default value is:
*
* ANNOTATION_DEF,
*
* ANNOTATION_FIELD_DEF,
*
* CLASS_DEF,
*
* CTOR_DEF,
*
* ENUM_CONSTANT_DEF,
*
* ENUM_DEF,
*
* INTERFACE_DEF,
*
* METHOD_DEF,
*
* PACKAGE_DEF,
*
* VARIABLE_DEF,
*
* RECORD_DEF,
*
* COMPACT_CTOR_DEF.
*
*
*
*
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
*
*
*
* Violation Message Keys:
*
*
* -
* {@code javadoc.empty}
*
* -
* {@code javadoc.extraHtml}
*
* -
* {@code javadoc.incompleteTag}
*
* -
* {@code javadoc.noPeriod}
*
* -
* {@code javadoc.unclosedHtml}
*
*
*
* @since 3.2
*/
@StatelessCheck
public class JavadocStyleCheck
extends AbstractCheck {
/** Message property key for the Empty Javadoc message. */
public static final String MSG_EMPTY = "javadoc.empty";
/** Message property key for the No Javadoc end of Sentence Period message. */
public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
/** Message property key for the Incomplete Tag message. */
public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
/** Message property key for the Unclosed HTML message. */
public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
/** Message property key for the Extra HTML message. */
public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
/** HTML tags that do not require a close tag. */
private static final Set SINGLE_TAGS = Set.of(
"br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th"
);
/**
* HTML tags that are allowed in java docs.
* From w3schools:
*
* The forms and structure tags are not allowed
*/
private static final Set ALLOWED_TAGS = Set.of(
"a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
"blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
"del", "dfn", "div", "dl", "dt", "em", "fieldset", "font", "h1",
"h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
"li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
"sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead",
"tr", "tt", "u", "ul", "var"
);
/** Specify the format for inline return Javadoc. */
private static final Pattern INLINE_RETURN_TAG_PATTERN =
Pattern.compile("\\{@return.*?}\\s*");
/** Specify the format for first word in javadoc. */
private static final Pattern SENTENCE_SEPARATOR = Pattern.compile("\\.(?=\\s|$)");
/** Specify the visibility scope where Javadoc comments are checked. */
private Scope scope = Scope.PRIVATE;
/** Specify the visibility scope where Javadoc comments are not checked. */
private Scope excludeScope;
/** Specify the format for matching the end of a sentence. */
private Pattern endOfSentenceFormat = Pattern.compile("([.?!][ \t\n\r\f<])|([.?!]$)");
/**
* Control whether to check the first sentence for proper end of sentence.
*/
private boolean checkFirstSentence = true;
/**
* Control whether to check for incomplete HTML tags.
*/
private boolean checkHtml = true;
/**
* Control whether to check if the Javadoc is missing a describing text.
*/
private boolean checkEmptyJavadoc;
@Override
public int[] getDefaultTokens() {
return getAcceptableTokens();
}
@Override
public int[] getAcceptableTokens() {
return new int[] {
TokenTypes.ANNOTATION_DEF,
TokenTypes.ANNOTATION_FIELD_DEF,
TokenTypes.CLASS_DEF,
TokenTypes.CTOR_DEF,
TokenTypes.ENUM_CONSTANT_DEF,
TokenTypes.ENUM_DEF,
TokenTypes.INTERFACE_DEF,
TokenTypes.METHOD_DEF,
TokenTypes.PACKAGE_DEF,
TokenTypes.VARIABLE_DEF,
TokenTypes.RECORD_DEF,
TokenTypes.COMPACT_CTOR_DEF,
};
}
@Override
public int[] getRequiredTokens() {
return CommonUtil.EMPTY_INT_ARRAY;
}
// suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
@SuppressWarnings("deprecation")
@Override
public void visitToken(DetailAST ast) {
if (shouldCheck(ast)) {
final FileContents contents = getFileContents();
// Need to start searching for the comment before the annotations
// that may exist. Even if annotations are not defined on the
// package, the ANNOTATIONS AST is defined.
final TextBlock textBlock =
contents.getJavadocBefore(ast.getFirstChild().getLineNo());
checkComment(ast, textBlock);
}
}
/**
* Whether we should check this node.
*
* @param ast a given node.
* @return whether we should check a given node.
*/
private boolean shouldCheck(final DetailAST ast) {
boolean check = false;
if (ast.getType() == TokenTypes.PACKAGE_DEF) {
check = CheckUtil.isPackageInfo(getFilePath());
}
else if (!ScopeUtil.isInCodeBlock(ast)) {
final Scope customScope = ScopeUtil.getScope(ast);
final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
check = customScope.isIn(scope)
&& surroundingScope.isIn(scope)
&& (excludeScope == null || !customScope.isIn(excludeScope)
|| !surroundingScope.isIn(excludeScope));
}
return check;
}
/**
* Performs the various checks against the Javadoc comment.
*
* @param ast the AST of the element being documented
* @param comment the source lines that make up the Javadoc comment.
*
* @see #checkFirstSentenceEnding(DetailAST, TextBlock)
* @see #checkHtmlTags(DetailAST, TextBlock)
*/
private void checkComment(final DetailAST ast, final TextBlock comment) {
if (comment != null) {
if (checkFirstSentence) {
checkFirstSentenceEnding(ast, comment);
}
if (checkHtml) {
checkHtmlTags(ast, comment);
}
if (checkEmptyJavadoc) {
checkJavadocIsNotEmpty(comment);
}
}
}
/**
* Checks that the first sentence ends with proper punctuation. This method
* uses a regular expression that checks for the presence of a period,
* question mark, or exclamation mark followed either by whitespace, an
* HTML element, or the end of string. This method ignores {_AT_inheritDoc}
* comments for TokenTypes that are valid for {_AT_inheritDoc}.
*
* @param ast the current node
* @param comment the source lines that make up the Javadoc comment.
*/
private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
final String commentText = getCommentText(comment.getText());
final boolean hasInLineReturnTag = Arrays.stream(SENTENCE_SEPARATOR.split(commentText))
.findFirst()
.map(INLINE_RETURN_TAG_PATTERN::matcher)
.filter(Matcher::find)
.isPresent();
if (!hasInLineReturnTag
&& !commentText.isEmpty()
&& !endOfSentenceFormat.matcher(commentText).find()
&& !(commentText.startsWith("{@inheritDoc}")
&& JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
log(comment.getStartLineNo(), MSG_NO_PERIOD);
}
}
/**
* Checks that the Javadoc is not empty.
*
* @param comment the source lines that make up the Javadoc comment.
*/
private void checkJavadocIsNotEmpty(TextBlock comment) {
final String commentText = getCommentText(comment.getText());
if (commentText.isEmpty()) {
log(comment.getStartLineNo(), MSG_EMPTY);
}
}
/**
* Returns the comment text from the Javadoc.
*
* @param comments the lines of Javadoc.
* @return a comment text String.
*/
private static String getCommentText(String... comments) {
final StringBuilder builder = new StringBuilder(1024);
for (final String line : comments) {
final int textStart = findTextStart(line);
if (textStart != -1) {
if (line.charAt(textStart) == '@') {
// we have found the tag section
break;
}
builder.append(line.substring(textStart));
trimTail(builder);
builder.append('\n');
}
}
return builder.toString().trim();
}
/**
* Finds the index of the first non-whitespace character ignoring the
* Javadoc comment start and end strings (/** and */) as well as any
* leading asterisk.
*
* @param line the Javadoc comment line of text to scan.
* @return the int index relative to 0 for the start of text
* or -1 if not found.
*/
private static int findTextStart(String line) {
int textStart = -1;
int index = 0;
while (index < line.length()) {
if (!Character.isWhitespace(line.charAt(index))) {
if (line.regionMatches(index, "/**", 0, "/**".length())
|| line.regionMatches(index, "*/", 0, 2)) {
index++;
}
else if (line.charAt(index) != '*') {
textStart = index;
break;
}
}
index++;
}
return textStart;
}
/**
* Trims any trailing whitespace or the end of Javadoc comment string.
*
* @param builder the StringBuilder to trim.
*/
private static void trimTail(StringBuilder builder) {
int index = builder.length() - 1;
while (true) {
if (Character.isWhitespace(builder.charAt(index))) {
builder.deleteCharAt(index);
}
else if (index > 0 && builder.charAt(index) == '/'
&& builder.charAt(index - 1) == '*') {
builder.deleteCharAt(index);
builder.deleteCharAt(index - 1);
index--;
while (builder.charAt(index - 1) == '*') {
builder.deleteCharAt(index - 1);
index--;
}
}
else {
break;
}
index--;
}
}
/**
* Checks the comment for HTML tags that do not have a corresponding close
* tag or a close tag that has no previous open tag. This code was
* primarily copied from the DocCheck checkHtml method.
*
* @param ast the node with the Javadoc
* @param comment the {@code TextBlock} which represents
* the Javadoc comment.
* @noinspection MethodWithMultipleReturnPoints
* @noinspectionreason MethodWithMultipleReturnPoints - check and method are
* too complex to break apart
*/
// -@cs[ReturnCount] Too complex to break apart.
private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
final int lineNo = comment.getStartLineNo();
final Deque htmlStack = new ArrayDeque<>();
final String[] text = comment.getText();
final TagParser parser = new TagParser(text, lineNo);
while (parser.hasNextTag()) {
final HtmlTag tag = parser.nextTag();
if (tag.isIncompleteTag()) {
log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
text[tag.getLineNo() - lineNo]);
return;
}
if (tag.isClosedTag()) {
// do nothing
continue;
}
if (tag.isCloseTag()) {
// We have found a close tag.
if (isExtraHtml(tag.getId(), htmlStack)) {
// No corresponding open tag was found on the stack.
log(tag.getLineNo(),
tag.getPosition(),
MSG_EXTRA_HTML,
tag.getText());
}
else {
// See if there are any unclosed tags that were opened
// after this one.
checkUnclosedTags(htmlStack, tag.getId());
}
}
else {
// We only push html tags that are allowed
if (isAllowedTag(tag)) {
htmlStack.push(tag);
}
}
}
// Identify any tags left on the stack.
// Skip multiples, like ...
String lastFound = "";
final List typeParameters = CheckUtil.getTypeParameterNames(ast);
for (final HtmlTag htmlTag : htmlStack) {
if (!isSingleTag(htmlTag)
&& !htmlTag.getId().equals(lastFound)
&& !typeParameters.contains(htmlTag.getId())) {
log(htmlTag.getLineNo(), htmlTag.getPosition(),
MSG_UNCLOSED_HTML, htmlTag.getText());
lastFound = htmlTag.getId();
}
}
}
/**
* Checks to see if there are any unclosed tags on the stack. The token
* represents a html tag that has been closed and has a corresponding open
* tag on the stack. Any tags, except single tags, that were opened
* (pushed on the stack) after the token are missing a close.
*
* @param htmlStack the stack of opened HTML tags.
* @param token the current HTML tag name that has been closed.
*/
private void checkUnclosedTags(Deque htmlStack, String token) {
final Deque unclosedTags = new ArrayDeque<>();
HtmlTag lastOpenTag = htmlStack.pop();
while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
// Find unclosed elements. Put them on a stack so the
// output order won't be back-to-front.
if (isSingleTag(lastOpenTag)) {
lastOpenTag = htmlStack.pop();
}
else {
unclosedTags.push(lastOpenTag);
lastOpenTag = htmlStack.pop();
}
}
// Output the unterminated tags, if any
// Skip multiples, like ..
String lastFound = "";
for (final HtmlTag htag : unclosedTags) {
lastOpenTag = htag;
if (lastOpenTag.getId().equals(lastFound)) {
continue;
}
lastFound = lastOpenTag.getId();
log(lastOpenTag.getLineNo(),
lastOpenTag.getPosition(),
MSG_UNCLOSED_HTML,
lastOpenTag.getText());
}
}
/**
* Determines if the HtmlTag is one which does not require a close tag.
*
* @param tag the HtmlTag to check.
* @return {@code true} if the HtmlTag is a single tag.
*/
private static boolean isSingleTag(HtmlTag tag) {
// If it's a singleton tag (,
, etc.), ignore it
// Can't simply not put them on the stack, since singletons
// like
and (unhappily) may either be terminated
// or not terminated. Both options are legal.
return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
}
/**
* Determines if the HtmlTag is one which is allowed in a javadoc.
*
* @param tag the HtmlTag to check.
* @return {@code true} if the HtmlTag is an allowed html tag.
*/
private static boolean isAllowedTag(HtmlTag tag) {
return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
}
/**
* Determines if the given token is an extra HTML tag. This indicates that
* a close tag was found that does not have a corresponding open tag.
*
* @param token an HTML tag id for which a close was found.
* @param htmlStack a Stack of previous open HTML tags.
* @return {@code false} if a previous open tag was found
* for the token.
*/
private static boolean isExtraHtml(String token, Deque htmlStack) {
boolean isExtra = true;
for (final HtmlTag tag : htmlStack) {
// Loop, looking for tags that are closed.
// The loop is needed in case there are unclosed
// tags on the stack. In that case, the stack would
// not be empty, but this tag would still be extra.
if (token.equalsIgnoreCase(tag.getId())) {
isExtra = false;
break;
}
}
return isExtra;
}
/**
* Setter to specify the visibility scope where Javadoc comments are checked.
*
* @param scope a scope.
* @since 3.2
*/
public void setScope(Scope scope) {
this.scope = scope;
}
/**
* Setter to specify the visibility scope where Javadoc comments are not checked.
*
* @param excludeScope a scope.
* @since 3.4
*/
public void setExcludeScope(Scope excludeScope) {
this.excludeScope = excludeScope;
}
/**
* Setter to specify the format for matching the end of a sentence.
*
* @param pattern a pattern.
* @since 5.0
*/
public void setEndOfSentenceFormat(Pattern pattern) {
endOfSentenceFormat = pattern;
}
/**
* Setter to control whether to check the first sentence for proper end of sentence.
*
* @param flag {@code true} if the first sentence is to be checked
* @since 3.2
*/
public void setCheckFirstSentence(boolean flag) {
checkFirstSentence = flag;
}
/**
* Setter to control whether to check for incomplete HTML tags.
*
* @param flag {@code true} if HTML checking is to be performed.
* @since 3.2
*/
public void setCheckHtml(boolean flag) {
checkHtml = flag;
}
/**
* Setter to control whether to check if the Javadoc is missing a describing text.
*
* @param flag {@code true} if empty Javadoc checking should be done.
* @since 3.4
*/
public void setCheckEmptyJavadoc(boolean flag) {
checkEmptyJavadoc = flag;
}
}