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