View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayDeque;
23  import java.util.Arrays;
24  import java.util.Deque;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Set;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
32  import com.puppycrawl.tools.checkstyle.StatelessCheck;
33  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.FileContents;
36  import com.puppycrawl.tools.checkstyle.api.Scope;
37  import com.puppycrawl.tools.checkstyle.api.TextBlock;
38  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
39  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
40  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
42  
43  /**
44   * <p>
45   * Validates Javadoc comments to help ensure they are well formed.
46   * </p>
47   * <p>
48   * The following checks are performed:
49   * </p>
50   * <ul>
51   * <li>
52   * Ensures the first sentence ends with proper punctuation
53   * (That is a period, question mark, or exclamation mark, by default).
54   * Note that this check is not applied to inline {@code @return} tags,
55   * because the Javadoc tools automatically appends a period to the end of the tag
56   * content.
57   * Javadoc automatically places the first sentence in the method summary
58   * table and index. Without proper punctuation the Javadoc may be malformed.
59   * All items eligible for the {@code {@inheritDoc}} tag are exempt from this
60   * requirement.
61   * </li>
62   * <li>
63   * Check text for Javadoc statements that do not have any description.
64   * This includes both completely empty Javadoc, and Javadoc with only tags
65   * such as {@code @param} and {@code @return}.
66   * </li>
67   * <li>
68   * Check text for incomplete HTML tags. Verifies that HTML tags have
69   * corresponding end tags and issues an "Unclosed HTML tag found:" error if not.
70   * An "Extra HTML tag found:" error is issued if an end tag is found without
71   * a previous open tag.
72   * </li>
73   * <li>
74   * Check that a package Javadoc comment is well-formed (as described above).
75   * </li>
76   * <li>
77   * Check for allowed HTML tags. The list of allowed HTML tags is
78   * "a", "abbr", "acronym", "address", "area", "b", "bdo", "big", "blockquote",
79   * "br", "caption", "cite", "code", "colgroup", "dd", "del", "dfn", "div", "dl",
80   * "dt", "em", "fieldset", "font", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
81   * "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "samp", "small",
82   * "span", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
83   * "thead", "tr", "tt", "u", "ul", "var".
84   * </li>
85   * </ul>
86   * <p>
87   * These checks were patterned after the checks made by the
88   * <a href="https://maven-doccheck.sourceforge.net">DocCheck</a> doclet
89   * available from Sun. Note: Original Sun's DocCheck tool does not exist anymore.
90   * </p>
91   * <ul>
92   * <li>
93   * Property {@code checkEmptyJavadoc} - Control whether to check if the Javadoc
94   * is missing a describing text.
95   * Type is {@code boolean}.
96   * Default value is {@code false}.
97   * </li>
98   * <li>
99   * Property {@code checkFirstSentence} - Control whether to check the first
100  * sentence for proper end of sentence.
101  * Type is {@code boolean}.
102  * Default value is {@code true}.
103  * </li>
104  * <li>
105  * Property {@code checkHtml} - Control whether to check for incomplete HTML tags.
106  * Type is {@code boolean}.
107  * Default value is {@code true}.
108  * </li>
109  * <li>
110  * Property {@code endOfSentenceFormat} - Specify the format for matching
111  * the end of a sentence.
112  * Type is {@code java.util.regex.Pattern}.
113  * Default value is {@code "([.?!][ \t\n\r\f&lt;])|([.?!]$)"}.
114  * </li>
115  * <li>
116  * Property {@code excludeScope} - Specify the visibility scope where
117  * Javadoc comments are not checked.
118  * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
119  * Default value is {@code null}.
120  * </li>
121  * <li>
122  * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
123  * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
124  * Default value is {@code private}.
125  * </li>
126  * <li>
127  * Property {@code tokens} - tokens to check
128  * Type is {@code java.lang.String[]}.
129  * Validation type is {@code tokenSet}.
130  * Default value is:
131  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
132  * ANNOTATION_DEF</a>,
133  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
134  * ANNOTATION_FIELD_DEF</a>,
135  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
136  * CLASS_DEF</a>,
137  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
138  * CTOR_DEF</a>,
139  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
140  * ENUM_CONSTANT_DEF</a>,
141  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
142  * ENUM_DEF</a>,
143  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
144  * INTERFACE_DEF</a>,
145  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
146  * METHOD_DEF</a>,
147  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PACKAGE_DEF">
148  * PACKAGE_DEF</a>,
149  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
150  * VARIABLE_DEF</a>,
151  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
152  * RECORD_DEF</a>,
153  * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
154  * COMPACT_CTOR_DEF</a>.
155  * </li>
156  * </ul>
157  * <p>
158  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
159  * </p>
160  * <p>
161  * Violation Message Keys:
162  * </p>
163  * <ul>
164  * <li>
165  * {@code javadoc.empty}
166  * </li>
167  * <li>
168  * {@code javadoc.extraHtml}
169  * </li>
170  * <li>
171  * {@code javadoc.incompleteTag}
172  * </li>
173  * <li>
174  * {@code javadoc.noPeriod}
175  * </li>
176  * <li>
177  * {@code javadoc.unclosedHtml}
178  * </li>
179  * </ul>
180  *
181  * @since 3.2
182  */
183 @StatelessCheck
184 public class JavadocStyleCheck
185     extends AbstractCheck {
186 
187     /** Message property key for the Empty Javadoc message. */
188     public static final String MSG_EMPTY = "javadoc.empty";
189 
190     /** Message property key for the No Javadoc end of Sentence Period message. */
191     public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
192 
193     /** Message property key for the Incomplete Tag message. */
194     public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
195 
196     /** Message property key for the Unclosed HTML message. */
197     public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
198 
199     /** Message property key for the Extra HTML message. */
200     public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
201 
202     /** HTML tags that do not require a close tag. */
203     private static final Set<String> SINGLE_TAGS = Set.of(
204         "br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th"
205     );
206 
207     /**
208      * HTML tags that are allowed in java docs.
209      * From <a href="https://www.w3schools.com/tags/default.asp">w3schools</a>:
210      * <br>
211      * The forms and structure tags are not allowed
212      */
213     private static final Set<String> ALLOWED_TAGS = Set.of(
214         "a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
215         "blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
216         "del", "dfn", "div", "dl", "dt", "em", "fieldset", "font", "h1",
217         "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
218         "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
219         "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead",
220         "tr", "tt", "u", "ul", "var"
221     );
222 
223     /** Specify the format for inline return Javadoc. */
224     private static final Pattern INLINE_RETURN_TAG_PATTERN =
225             Pattern.compile("\\{@return.*?}\\s*");
226 
227     /** Specify the format for first word in javadoc. */
228     private static final Pattern SENTENCE_SEPARATOR = Pattern.compile("\\.(?=\\s|$)");
229 
230     /** Specify the visibility scope where Javadoc comments are checked. */
231     private Scope scope = Scope.PRIVATE;
232 
233     /** Specify the visibility scope where Javadoc comments are not checked. */
234     private Scope excludeScope;
235 
236     /** Specify the format for matching the end of a sentence. */
237     private Pattern endOfSentenceFormat = Pattern.compile("([.?!][ \t\n\r\f<])|([.?!]$)");
238 
239     /**
240      * Control whether to check the first sentence for proper end of sentence.
241      */
242     private boolean checkFirstSentence = true;
243 
244     /**
245      * Control whether to check for incomplete HTML tags.
246      */
247     private boolean checkHtml = true;
248 
249     /**
250      * Control whether to check if the Javadoc is missing a describing text.
251      */
252     private boolean checkEmptyJavadoc;
253 
254     @Override
255     public int[] getDefaultTokens() {
256         return getAcceptableTokens();
257     }
258 
259     @Override
260     public int[] getAcceptableTokens() {
261         return new int[] {
262             TokenTypes.ANNOTATION_DEF,
263             TokenTypes.ANNOTATION_FIELD_DEF,
264             TokenTypes.CLASS_DEF,
265             TokenTypes.CTOR_DEF,
266             TokenTypes.ENUM_CONSTANT_DEF,
267             TokenTypes.ENUM_DEF,
268             TokenTypes.INTERFACE_DEF,
269             TokenTypes.METHOD_DEF,
270             TokenTypes.PACKAGE_DEF,
271             TokenTypes.VARIABLE_DEF,
272             TokenTypes.RECORD_DEF,
273             TokenTypes.COMPACT_CTOR_DEF,
274         };
275     }
276 
277     @Override
278     public int[] getRequiredTokens() {
279         return CommonUtil.EMPTY_INT_ARRAY;
280     }
281 
282     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
283     @SuppressWarnings("deprecation")
284     @Override
285     public void visitToken(DetailAST ast) {
286         if (shouldCheck(ast)) {
287             final FileContents contents = getFileContents();
288             // Need to start searching for the comment before the annotations
289             // that may exist. Even if annotations are not defined on the
290             // package, the ANNOTATIONS AST is defined.
291             final TextBlock textBlock =
292                 contents.getJavadocBefore(ast.getFirstChild().getLineNo());
293 
294             checkComment(ast, textBlock);
295         }
296     }
297 
298     /**
299      * Whether we should check this node.
300      *
301      * @param ast a given node.
302      * @return whether we should check a given node.
303      */
304     private boolean shouldCheck(final DetailAST ast) {
305         boolean check = false;
306 
307         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
308             check = CheckUtil.isPackageInfo(getFilePath());
309         }
310         else if (!ScopeUtil.isInCodeBlock(ast)) {
311             final Scope customScope = ScopeUtil.getScope(ast);
312             final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
313 
314             check = customScope.isIn(scope)
315                     && surroundingScope.isIn(scope)
316                     && (excludeScope == null || !customScope.isIn(excludeScope)
317                             || !surroundingScope.isIn(excludeScope));
318         }
319         return check;
320     }
321 
322     /**
323      * Performs the various checks against the Javadoc comment.
324      *
325      * @param ast the AST of the element being documented
326      * @param comment the source lines that make up the Javadoc comment.
327      *
328      * @see #checkFirstSentenceEnding(DetailAST, TextBlock)
329      * @see #checkHtmlTags(DetailAST, TextBlock)
330      */
331     private void checkComment(final DetailAST ast, final TextBlock comment) {
332         if (comment != null) {
333             if (checkFirstSentence) {
334                 checkFirstSentenceEnding(ast, comment);
335             }
336 
337             if (checkHtml) {
338                 checkHtmlTags(ast, comment);
339             }
340 
341             if (checkEmptyJavadoc) {
342                 checkJavadocIsNotEmpty(comment);
343             }
344         }
345     }
346 
347     /**
348      * Checks that the first sentence ends with proper punctuation.  This method
349      * uses a regular expression that checks for the presence of a period,
350      * question mark, or exclamation mark followed either by whitespace, an
351      * HTML element, or the end of string. This method ignores {_AT_inheritDoc}
352      * comments for TokenTypes that are valid for {_AT_inheritDoc}.
353      *
354      * @param ast the current node
355      * @param comment the source lines that make up the Javadoc comment.
356      */
357     private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
358         final String commentText = getCommentText(comment.getText());
359         final boolean hasInLineReturnTag = Arrays.stream(SENTENCE_SEPARATOR.split(commentText))
360                 .findFirst()
361                 .map(INLINE_RETURN_TAG_PATTERN::matcher)
362                 .filter(Matcher::find)
363                 .isPresent();
364 
365         if (!hasInLineReturnTag
366             && !commentText.isEmpty()
367             && !endOfSentenceFormat.matcher(commentText).find()
368             && !(commentText.startsWith("{@inheritDoc}")
369             && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
370             log(comment.getStartLineNo(), MSG_NO_PERIOD);
371         }
372     }
373 
374     /**
375      * Checks that the Javadoc is not empty.
376      *
377      * @param comment the source lines that make up the Javadoc comment.
378      */
379     private void checkJavadocIsNotEmpty(TextBlock comment) {
380         final String commentText = getCommentText(comment.getText());
381 
382         if (commentText.isEmpty()) {
383             log(comment.getStartLineNo(), MSG_EMPTY);
384         }
385     }
386 
387     /**
388      * Returns the comment text from the Javadoc.
389      *
390      * @param comments the lines of Javadoc.
391      * @return a comment text String.
392      */
393     private static String getCommentText(String... comments) {
394         final StringBuilder builder = new StringBuilder(1024);
395         for (final String line : comments) {
396             final int textStart = findTextStart(line);
397 
398             if (textStart != -1) {
399                 if (line.charAt(textStart) == '@') {
400                     // we have found the tag section
401                     break;
402                 }
403                 builder.append(line.substring(textStart));
404                 trimTail(builder);
405                 builder.append('\n');
406             }
407         }
408 
409         return builder.toString().trim();
410     }
411 
412     /**
413      * Finds the index of the first non-whitespace character ignoring the
414      * Javadoc comment start and end strings (&#47;** and *&#47;) as well as any
415      * leading asterisk.
416      *
417      * @param line the Javadoc comment line of text to scan.
418      * @return the int index relative to 0 for the start of text
419      *         or -1 if not found.
420      */
421     private static int findTextStart(String line) {
422         int textStart = -1;
423         int index = 0;
424         while (index < line.length()) {
425             if (!Character.isWhitespace(line.charAt(index))) {
426                 if (line.regionMatches(index, "/**", 0, "/**".length())
427                     || line.regionMatches(index, "*/", 0, 2)) {
428                     index++;
429                 }
430                 else if (line.charAt(index) != '*') {
431                     textStart = index;
432                     break;
433                 }
434             }
435             index++;
436         }
437         return textStart;
438     }
439 
440     /**
441      * Trims any trailing whitespace or the end of Javadoc comment string.
442      *
443      * @param builder the StringBuilder to trim.
444      */
445     private static void trimTail(StringBuilder builder) {
446         int index = builder.length() - 1;
447         while (true) {
448             if (Character.isWhitespace(builder.charAt(index))) {
449                 builder.deleteCharAt(index);
450             }
451             else if (index > 0 && builder.charAt(index) == '/'
452                     && builder.charAt(index - 1) == '*') {
453                 builder.deleteCharAt(index);
454                 builder.deleteCharAt(index - 1);
455                 index--;
456                 while (builder.charAt(index - 1) == '*') {
457                     builder.deleteCharAt(index - 1);
458                     index--;
459                 }
460             }
461             else {
462                 break;
463             }
464             index--;
465         }
466     }
467 
468     /**
469      * Checks the comment for HTML tags that do not have a corresponding close
470      * tag or a close tag that has no previous open tag.  This code was
471      * primarily copied from the DocCheck checkHtml method.
472      *
473      * @param ast the node with the Javadoc
474      * @param comment the {@code TextBlock} which represents
475      *                 the Javadoc comment.
476      * @noinspection MethodWithMultipleReturnPoints
477      * @noinspectionreason MethodWithMultipleReturnPoints - check and method are
478      *      too complex to break apart
479      */
480     // -@cs[ReturnCount] Too complex to break apart.
481     private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
482         final int lineNo = comment.getStartLineNo();
483         final Deque<HtmlTag> htmlStack = new ArrayDeque<>();
484         final String[] text = comment.getText();
485 
486         final TagParser parser = new TagParser(text, lineNo);
487 
488         while (parser.hasNextTag()) {
489             final HtmlTag tag = parser.nextTag();
490 
491             if (tag.isIncompleteTag()) {
492                 log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
493                     text[tag.getLineNo() - lineNo]);
494                 return;
495             }
496             if (tag.isClosedTag()) {
497                 // do nothing
498                 continue;
499             }
500             if (tag.isCloseTag()) {
501                 // We have found a close tag.
502                 if (isExtraHtml(tag.getId(), htmlStack)) {
503                     // No corresponding open tag was found on the stack.
504                     log(tag.getLineNo(),
505                         tag.getPosition(),
506                         MSG_EXTRA_HTML,
507                         tag.getText());
508                 }
509                 else {
510                     // See if there are any unclosed tags that were opened
511                     // after this one.
512                     checkUnclosedTags(htmlStack, tag.getId());
513                 }
514             }
515             else {
516                 // We only push html tags that are allowed
517                 if (isAllowedTag(tag)) {
518                     htmlStack.push(tag);
519                 }
520             }
521         }
522 
523         // Identify any tags left on the stack.
524         // Skip multiples, like <b>...<b>
525         String lastFound = "";
526         final List<String> typeParameters = CheckUtil.getTypeParameterNames(ast);
527         for (final HtmlTag htmlTag : htmlStack) {
528             if (!isSingleTag(htmlTag)
529                 && !htmlTag.getId().equals(lastFound)
530                 && !typeParameters.contains(htmlTag.getId())) {
531                 log(htmlTag.getLineNo(), htmlTag.getPosition(),
532                         MSG_UNCLOSED_HTML, htmlTag.getText());
533                 lastFound = htmlTag.getId();
534             }
535         }
536     }
537 
538     /**
539      * Checks to see if there are any unclosed tags on the stack.  The token
540      * represents a html tag that has been closed and has a corresponding open
541      * tag on the stack.  Any tags, except single tags, that were opened
542      * (pushed on the stack) after the token are missing a close.
543      *
544      * @param htmlStack the stack of opened HTML tags.
545      * @param token the current HTML tag name that has been closed.
546      */
547     private void checkUnclosedTags(Deque<HtmlTag> htmlStack, String token) {
548         final Deque<HtmlTag> unclosedTags = new ArrayDeque<>();
549         HtmlTag lastOpenTag = htmlStack.pop();
550         while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
551             // Find unclosed elements. Put them on a stack so the
552             // output order won't be back-to-front.
553             if (isSingleTag(lastOpenTag)) {
554                 lastOpenTag = htmlStack.pop();
555             }
556             else {
557                 unclosedTags.push(lastOpenTag);
558                 lastOpenTag = htmlStack.pop();
559             }
560         }
561 
562         // Output the unterminated tags, if any
563         // Skip multiples, like <b>..<b>
564         String lastFound = "";
565         for (final HtmlTag htag : unclosedTags) {
566             lastOpenTag = htag;
567             if (lastOpenTag.getId().equals(lastFound)) {
568                 continue;
569             }
570             lastFound = lastOpenTag.getId();
571             log(lastOpenTag.getLineNo(),
572                 lastOpenTag.getPosition(),
573                 MSG_UNCLOSED_HTML,
574                 lastOpenTag.getText());
575         }
576     }
577 
578     /**
579      * Determines if the HtmlTag is one which does not require a close tag.
580      *
581      * @param tag the HtmlTag to check.
582      * @return {@code true} if the HtmlTag is a single tag.
583      */
584     private static boolean isSingleTag(HtmlTag tag) {
585         // If it's a singleton tag (<p>, <br>, etc.), ignore it
586         // Can't simply not put them on the stack, since singletons
587         // like <dt> and <dd> (unhappily) may either be terminated
588         // or not terminated. Both options are legal.
589         return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
590     }
591 
592     /**
593      * Determines if the HtmlTag is one which is allowed in a javadoc.
594      *
595      * @param tag the HtmlTag to check.
596      * @return {@code true} if the HtmlTag is an allowed html tag.
597      */
598     private static boolean isAllowedTag(HtmlTag tag) {
599         return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
600     }
601 
602     /**
603      * Determines if the given token is an extra HTML tag. This indicates that
604      * a close tag was found that does not have a corresponding open tag.
605      *
606      * @param token an HTML tag id for which a close was found.
607      * @param htmlStack a Stack of previous open HTML tags.
608      * @return {@code false} if a previous open tag was found
609      *         for the token.
610      */
611     private static boolean isExtraHtml(String token, Deque<HtmlTag> htmlStack) {
612         boolean isExtra = true;
613         for (final HtmlTag tag : htmlStack) {
614             // Loop, looking for tags that are closed.
615             // The loop is needed in case there are unclosed
616             // tags on the stack. In that case, the stack would
617             // not be empty, but this tag would still be extra.
618             if (token.equalsIgnoreCase(tag.getId())) {
619                 isExtra = false;
620                 break;
621             }
622         }
623 
624         return isExtra;
625     }
626 
627     /**
628      * Setter to specify the visibility scope where Javadoc comments are checked.
629      *
630      * @param scope a scope.
631      * @since 3.2
632      */
633     public void setScope(Scope scope) {
634         this.scope = scope;
635     }
636 
637     /**
638      * Setter to specify the visibility scope where Javadoc comments are not checked.
639      *
640      * @param excludeScope a scope.
641      * @since 3.4
642      */
643     public void setExcludeScope(Scope excludeScope) {
644         this.excludeScope = excludeScope;
645     }
646 
647     /**
648      * Setter to specify the format for matching the end of a sentence.
649      *
650      * @param pattern a pattern.
651      * @since 5.0
652      */
653     public void setEndOfSentenceFormat(Pattern pattern) {
654         endOfSentenceFormat = pattern;
655     }
656 
657     /**
658      * Setter to control whether to check the first sentence for proper end of sentence.
659      *
660      * @param flag {@code true} if the first sentence is to be checked
661      * @since 3.2
662      */
663     public void setCheckFirstSentence(boolean flag) {
664         checkFirstSentence = flag;
665     }
666 
667     /**
668      * Setter to control whether to check for incomplete HTML tags.
669      *
670      * @param flag {@code true} if HTML checking is to be performed.
671      * @since 3.2
672      */
673     public void setCheckHtml(boolean flag) {
674         checkHtml = flag;
675     }
676 
677     /**
678      * Setter to control whether to check if the Javadoc is missing a describing text.
679      *
680      * @param flag {@code true} if empty Javadoc checking should be done.
681      * @since 3.4
682      */
683     public void setCheckEmptyJavadoc(boolean flag) {
684         checkEmptyJavadoc = flag;
685     }
686 
687 }