001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024import java.util.List;
025import java.util.Locale;
026import java.util.Set;
027import java.util.regex.Pattern;
028
029import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
030import com.puppycrawl.tools.checkstyle.StatelessCheck;
031import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.FileContents;
034import com.puppycrawl.tools.checkstyle.api.Scope;
035import com.puppycrawl.tools.checkstyle.api.TextBlock;
036import com.puppycrawl.tools.checkstyle.api.TokenTypes;
037import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
038import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
039import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
040
041/**
042 * <p>
043 * Validates Javadoc comments to help ensure they are well formed.
044 * </p>
045 * <p>
046 * The following checks are performed:
047 * </p>
048 * <ul>
049 * <li>
050 * Ensures the first sentence ends with proper punctuation
051 * (That is a period, question mark, or exclamation mark, by default).
052 * Javadoc automatically places the first sentence in the method summary
053 * table and index. Without proper punctuation the Javadoc may be malformed.
054 * All items eligible for the {@code {@inheritDoc}} tag are exempt from this
055 * requirement.
056 * </li>
057 * <li>
058 * Check text for Javadoc statements that do not have any description.
059 * This includes both completely empty Javadoc, and Javadoc with only tags
060 * such as {@code @param} and {@code @return}.
061 * </li>
062 * <li>
063 * Check text for incomplete HTML tags. Verifies that HTML tags have
064 * corresponding end tags and issues an "Unclosed HTML tag found:" error if not.
065 * An "Extra HTML tag found:" error is issued if an end tag is found without
066 * a previous open tag.
067 * </li>
068 * <li>
069 * Check that a package Javadoc comment is well-formed (as described above).
070 * </li>
071 * <li>
072 * Check for allowed HTML tags. The list of allowed HTML tags is
073 * "a", "abbr", "acronym", "address", "area", "b", "bdo", "big", "blockquote",
074 * "br", "caption", "cite", "code", "colgroup", "dd", "del", "dfn", "div", "dl",
075 * "dt", "em", "fieldset", "font", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
076 * "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "samp", "small",
077 * "span", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
078 * "thead", "tr", "tt", "u", "ul", "var".
079 * </li>
080 * </ul>
081 * <p>
082 * These checks were patterned after the checks made by the
083 * <a href="https://maven-doccheck.sourceforge.net">DocCheck</a> doclet
084 * available from Sun. Note: Original Sun's DocCheck tool does not exist anymore.
085 * </p>
086 * <ul>
087 * <li>
088 * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
089 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
090 * Default value is {@code private}.
091 * </li>
092 * <li>
093 * Property {@code excludeScope} - Specify the visibility scope where
094 * Javadoc comments are not checked.
095 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
096 * Default value is {@code null}.
097 * </li>
098 * <li>
099 * 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 endOfSentenceFormat} - Specify the format for matching
106 * the end of a sentence.
107 * Type is {@code java.util.regex.Pattern}.
108 * Default value is {@code "([.?!][ \t\n\r\f&lt;])|([.?!]$)"}.
109 * </li>
110 * <li>
111 * Property {@code checkEmptyJavadoc} - Control whether to check if the Javadoc
112 * is missing a describing text.
113 * Type is {@code boolean}.
114 * Default value is {@code false}.
115 * </li>
116 * <li>
117 * Property {@code checkHtml} - Control whether to check for incomplete HTML tags.
118 * Type is {@code boolean}.
119 * Default value is {@code true}.
120 * </li>
121 * <li>
122 * Property {@code tokens} - tokens to check
123 * Type is {@code java.lang.String[]}.
124 * Validation type is {@code tokenSet}.
125 * Default value is:
126 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
127 * ANNOTATION_DEF</a>,
128 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
129 * ANNOTATION_FIELD_DEF</a>,
130 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
131 * CLASS_DEF</a>,
132 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
133 * CTOR_DEF</a>,
134 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
135 * ENUM_CONSTANT_DEF</a>,
136 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
137 * ENUM_DEF</a>,
138 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
139 * INTERFACE_DEF</a>,
140 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
141 * METHOD_DEF</a>,
142 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PACKAGE_DEF">
143 * PACKAGE_DEF</a>,
144 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
145 * VARIABLE_DEF</a>,
146 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
147 * RECORD_DEF</a>,
148 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
149 * COMPACT_CTOR_DEF</a>.
150 * </li>
151 * </ul>
152 * <p>
153 * To configure the default check:
154 * </p>
155 * <pre>
156 * &lt;module name="JavadocStyle"/&gt;
157 * </pre>
158 * <p>Example:</p>
159 * <pre>
160 * public class Test {
161 *     &#47;**
162 *      * Some description here. // OK
163 *      *&#47;
164 *     private void methodWithValidCommentStyle() {}
165 *
166 *     &#47;**
167 *      * Some description here // violation, the sentence must end with a proper punctuation
168 *      *&#47;
169 *     private void methodWithInvalidCommentStyle() {}
170 * }
171 * </pre>
172 * <p>
173 * To configure the check for {@code public} scope:
174 * </p>
175 * <pre>
176 * &lt;module name="JavadocStyle"&gt;
177 *   &lt;property name="scope" value="public"/&gt;
178 * &lt;/module&gt;
179 * </pre>
180 * <p>Example:</p>
181 * <pre>
182 * public class Test {
183 *     &#47;**
184 *      * Some description here // violation, the sentence must end with a proper punctuation
185 *      *&#47;
186 *     public void test1() {}
187 *
188 *     &#47;**
189 *      * Some description here // OK
190 *      *&#47;
191 *     private void test2() {}
192 * }
193 * </pre>
194 * <p>
195 * To configure the check for javadoc which is in {@code private}, but not in {@code package} scope:
196 * </p>
197 * <pre>
198 * &lt;module name="JavadocStyle"&gt;
199 *   &lt;property name="scope" value="private"/&gt;
200 *   &lt;property name="excludeScope" value="package"/&gt;
201 * &lt;/module&gt;
202 * </pre>
203 * <p>Example:</p>
204 * <pre>
205 * public class Test {
206 *     &#47;**
207 *      * Some description here // violation, the sentence must end with a proper punctuation
208 *      *&#47;
209 *     private void test1() {}
210 *
211 *     &#47;**
212 *      * Some description here // OK
213 *      *&#47;
214 *     void test2() {}
215 * }
216 * </pre>
217 * <p>
218 * To configure the check to turn off first sentence checking:
219 * </p>
220 * <pre>
221 * &lt;module name="JavadocStyle"&gt;
222 *   &lt;property name="checkFirstSentence" value="false"/&gt;
223 * &lt;/module&gt;
224 * </pre>
225 * <p>Example:</p>
226 * <pre>
227 * public class Test {
228 *     &#47;**
229 *      * Some description here // OK
230 *      * Second line of description // violation, the sentence must end with a proper punctuation
231 *      *&#47;
232 *     private void test1() {}
233 * }
234 * </pre>
235 * <p>
236 * To configure the check to turn off validation of incomplete html tags:
237 * </p>
238 * <pre>
239 * &lt;module name="JavadocStyle"&gt;
240 * &lt;property name="checkHtml" value="false"/&gt;
241 * &lt;/module&gt;
242 * </pre>
243 * <p>Example:</p>
244 * <pre>
245 * public class Test {
246 *     &#47;**
247 *      * Some description here // violation, the sentence must end with a proper punctuation
248 *      * &lt;p // OK
249 *      *&#47;
250 *     private void test1() {}
251 * }
252 * </pre>
253 * <p>
254 * To configure the check for only class definitions:
255 * </p>
256 * <pre>
257 * &lt;module name="JavadocStyle"&gt;
258 * &lt;property name="tokens" value="CLASS_DEF"/&gt;
259 * &lt;/module&gt;
260 * </pre>
261 * <p>Example:</p>
262 * <pre>
263 * &#47;**
264 *  * Some description here // violation, the sentence must end with a proper punctuation
265 *  *&#47;
266 * public class Test {
267 *     &#47;**
268 *      * Some description here // OK
269 *      *&#47;
270 *     private void test1() {}
271 * }
272 * </pre>
273 * <p>
274 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
275 * </p>
276 * <p>
277 * Violation Message Keys:
278 * </p>
279 * <ul>
280 * <li>
281 * {@code javadoc.empty}
282 * </li>
283 * <li>
284 * {@code javadoc.extraHtml}
285 * </li>
286 * <li>
287 * {@code javadoc.incompleteTag}
288 * </li>
289 * <li>
290 * {@code javadoc.noPeriod}
291 * </li>
292 * <li>
293 * {@code javadoc.unclosedHtml}
294 * </li>
295 * </ul>
296 *
297 * @since 3.2
298 */
299@StatelessCheck
300public class JavadocStyleCheck
301    extends AbstractCheck {
302
303    /** Message property key for the Empty Javadoc message. */
304    public static final String MSG_EMPTY = "javadoc.empty";
305
306    /** Message property key for the No Javadoc end of Sentence Period message. */
307    public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
308
309    /** Message property key for the Incomplete Tag message. */
310    public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
311
312    /** Message property key for the Unclosed HTML message. */
313    public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
314
315    /** Message property key for the Extra HTML message. */
316    public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
317
318    /** HTML tags that do not require a close tag. */
319    private static final Set<String> SINGLE_TAGS = Set.of(
320        "br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th"
321    );
322
323    /**
324     * HTML tags that are allowed in java docs.
325     * From <a href="https://www.w3schools.com/tags/default.asp">w3schools</a>:
326     * <br>
327     * The forms and structure tags are not allowed
328     */
329    private static final Set<String> ALLOWED_TAGS = Set.of(
330        "a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
331        "blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
332        "del", "dfn", "div", "dl", "dt", "em", "fieldset", "font", "h1",
333        "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
334        "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
335        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead",
336        "tr", "tt", "u", "ul", "var"
337    );
338
339    /** Specify the visibility scope where Javadoc comments are checked. */
340    private Scope scope = Scope.PRIVATE;
341
342    /** Specify the visibility scope where Javadoc comments are not checked. */
343    private Scope excludeScope;
344
345    /** Specify the format for matching the end of a sentence. */
346    private Pattern endOfSentenceFormat = Pattern.compile("([.?!][ \t\n\r\f<])|([.?!]$)");
347
348    /**
349     * Control whether to check the first sentence for proper end of sentence.
350     */
351    private boolean checkFirstSentence = true;
352
353    /**
354     * Control whether to check for incomplete HTML tags.
355     */
356    private boolean checkHtml = true;
357
358    /**
359     * Control whether to check if the Javadoc is missing a describing text.
360     */
361    private boolean checkEmptyJavadoc;
362
363    @Override
364    public int[] getDefaultTokens() {
365        return getAcceptableTokens();
366    }
367
368    @Override
369    public int[] getAcceptableTokens() {
370        return new int[] {
371            TokenTypes.ANNOTATION_DEF,
372            TokenTypes.ANNOTATION_FIELD_DEF,
373            TokenTypes.CLASS_DEF,
374            TokenTypes.CTOR_DEF,
375            TokenTypes.ENUM_CONSTANT_DEF,
376            TokenTypes.ENUM_DEF,
377            TokenTypes.INTERFACE_DEF,
378            TokenTypes.METHOD_DEF,
379            TokenTypes.PACKAGE_DEF,
380            TokenTypes.VARIABLE_DEF,
381            TokenTypes.RECORD_DEF,
382            TokenTypes.COMPACT_CTOR_DEF,
383        };
384    }
385
386    @Override
387    public int[] getRequiredTokens() {
388        return CommonUtil.EMPTY_INT_ARRAY;
389    }
390
391    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
392    @SuppressWarnings("deprecation")
393    @Override
394    public void visitToken(DetailAST ast) {
395        if (shouldCheck(ast)) {
396            final FileContents contents = getFileContents();
397            // Need to start searching for the comment before the annotations
398            // that may exist. Even if annotations are not defined on the
399            // package, the ANNOTATIONS AST is defined.
400            final TextBlock textBlock =
401                contents.getJavadocBefore(ast.getFirstChild().getLineNo());
402
403            checkComment(ast, textBlock);
404        }
405    }
406
407    /**
408     * Whether we should check this node.
409     *
410     * @param ast a given node.
411     * @return whether we should check a given node.
412     */
413    private boolean shouldCheck(final DetailAST ast) {
414        boolean check = false;
415
416        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
417            check = CheckUtil.isPackageInfo(getFilePath());
418        }
419        else if (!ScopeUtil.isInCodeBlock(ast)) {
420            final Scope customScope = ScopeUtil.getScope(ast);
421            final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
422
423            check = customScope.isIn(scope)
424                    && (surroundingScope == null || surroundingScope.isIn(scope))
425                    && (excludeScope == null
426                        || !customScope.isIn(excludeScope)
427                        || surroundingScope != null
428                            && !surroundingScope.isIn(excludeScope));
429        }
430        return check;
431    }
432
433    /**
434     * Performs the various checks against the Javadoc comment.
435     *
436     * @param ast the AST of the element being documented
437     * @param comment the source lines that make up the Javadoc comment.
438     *
439     * @see #checkFirstSentenceEnding(DetailAST, TextBlock)
440     * @see #checkHtmlTags(DetailAST, TextBlock)
441     */
442    private void checkComment(final DetailAST ast, final TextBlock comment) {
443        if (comment != null) {
444            if (checkFirstSentence) {
445                checkFirstSentenceEnding(ast, comment);
446            }
447
448            if (checkHtml) {
449                checkHtmlTags(ast, comment);
450            }
451
452            if (checkEmptyJavadoc) {
453                checkJavadocIsNotEmpty(comment);
454            }
455        }
456    }
457
458    /**
459     * Checks that the first sentence ends with proper punctuation.  This method
460     * uses a regular expression that checks for the presence of a period,
461     * question mark, or exclamation mark followed either by whitespace, an
462     * HTML element, or the end of string. This method ignores {_AT_inheritDoc}
463     * comments for TokenTypes that are valid for {_AT_inheritDoc}.
464     *
465     * @param ast the current node
466     * @param comment the source lines that make up the Javadoc comment.
467     */
468    private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
469        final String commentText = getCommentText(comment.getText());
470
471        if (!commentText.isEmpty()
472            && !endOfSentenceFormat.matcher(commentText).find()
473            && !(commentText.startsWith("{@inheritDoc}")
474            && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
475            log(comment.getStartLineNo(), MSG_NO_PERIOD);
476        }
477    }
478
479    /**
480     * Checks that the Javadoc is not empty.
481     *
482     * @param comment the source lines that make up the Javadoc comment.
483     */
484    private void checkJavadocIsNotEmpty(TextBlock comment) {
485        final String commentText = getCommentText(comment.getText());
486
487        if (commentText.isEmpty()) {
488            log(comment.getStartLineNo(), MSG_EMPTY);
489        }
490    }
491
492    /**
493     * Returns the comment text from the Javadoc.
494     *
495     * @param comments the lines of Javadoc.
496     * @return a comment text String.
497     */
498    private static String getCommentText(String... comments) {
499        final StringBuilder builder = new StringBuilder(1024);
500        for (final String line : comments) {
501            final int textStart = findTextStart(line);
502
503            if (textStart != -1) {
504                if (line.charAt(textStart) == '@') {
505                    // we have found the tag section
506                    break;
507                }
508                builder.append(line.substring(textStart));
509                trimTail(builder);
510                builder.append('\n');
511            }
512        }
513
514        return builder.toString().trim();
515    }
516
517    /**
518     * Finds the index of the first non-whitespace character ignoring the
519     * Javadoc comment start and end strings (&#47;** and *&#47;) as well as any
520     * leading asterisk.
521     *
522     * @param line the Javadoc comment line of text to scan.
523     * @return the int index relative to 0 for the start of text
524     *         or -1 if not found.
525     */
526    private static int findTextStart(String line) {
527        int textStart = -1;
528        int index = 0;
529        while (index < line.length()) {
530            if (!Character.isWhitespace(line.charAt(index))) {
531                if (line.regionMatches(index, "/**", 0, "/**".length())) {
532                    index += 2;
533                }
534                else if (line.regionMatches(index, "*/", 0, 2)) {
535                    index++;
536                }
537                else if (line.charAt(index) != '*') {
538                    textStart = index;
539                    break;
540                }
541            }
542            index++;
543        }
544        return textStart;
545    }
546
547    /**
548     * Trims any trailing whitespace or the end of Javadoc comment string.
549     *
550     * @param builder the StringBuilder to trim.
551     */
552    private static void trimTail(StringBuilder builder) {
553        int index = builder.length() - 1;
554        while (true) {
555            if (Character.isWhitespace(builder.charAt(index))) {
556                builder.deleteCharAt(index);
557            }
558            else if (index > 0 && builder.charAt(index) == '/'
559                    && builder.charAt(index - 1) == '*') {
560                builder.deleteCharAt(index);
561                builder.deleteCharAt(index - 1);
562                index--;
563                while (builder.charAt(index - 1) == '*') {
564                    builder.deleteCharAt(index - 1);
565                    index--;
566                }
567            }
568            else {
569                break;
570            }
571            index--;
572        }
573    }
574
575    /**
576     * Checks the comment for HTML tags that do not have a corresponding close
577     * tag or a close tag that has no previous open tag.  This code was
578     * primarily copied from the DocCheck checkHtml method.
579     *
580     * @param ast the node with the Javadoc
581     * @param comment the {@code TextBlock} which represents
582     *                 the Javadoc comment.
583     * @noinspection MethodWithMultipleReturnPoints
584     * @noinspectionreason MethodWithMultipleReturnPoints - check and method are
585     *      too complex to break apart
586     */
587    // -@cs[ReturnCount] Too complex to break apart.
588    private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
589        final int lineNo = comment.getStartLineNo();
590        final Deque<HtmlTag> htmlStack = new ArrayDeque<>();
591        final String[] text = comment.getText();
592
593        final TagParser parser = new TagParser(text, lineNo);
594
595        while (parser.hasNextTag()) {
596            final HtmlTag tag = parser.nextTag();
597
598            if (tag.isIncompleteTag()) {
599                log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
600                    text[tag.getLineNo() - lineNo]);
601                return;
602            }
603            if (tag.isClosedTag()) {
604                // do nothing
605                continue;
606            }
607            if (tag.isCloseTag()) {
608                // We have found a close tag.
609                if (isExtraHtml(tag.getId(), htmlStack)) {
610                    // No corresponding open tag was found on the stack.
611                    log(tag.getLineNo(),
612                        tag.getPosition(),
613                        MSG_EXTRA_HTML,
614                        tag.getText());
615                }
616                else {
617                    // See if there are any unclosed tags that were opened
618                    // after this one.
619                    checkUnclosedTags(htmlStack, tag.getId());
620                }
621            }
622            else {
623                // We only push html tags that are allowed
624                if (isAllowedTag(tag)) {
625                    htmlStack.push(tag);
626                }
627            }
628        }
629
630        // Identify any tags left on the stack.
631        // Skip multiples, like <b>...<b>
632        String lastFound = "";
633        final List<String> typeParameters = CheckUtil.getTypeParameterNames(ast);
634        for (final HtmlTag htmlTag : htmlStack) {
635            if (!isSingleTag(htmlTag)
636                && !htmlTag.getId().equals(lastFound)
637                && !typeParameters.contains(htmlTag.getId())) {
638                log(htmlTag.getLineNo(), htmlTag.getPosition(),
639                        MSG_UNCLOSED_HTML, htmlTag.getText());
640                lastFound = htmlTag.getId();
641            }
642        }
643    }
644
645    /**
646     * Checks to see if there are any unclosed tags on the stack.  The token
647     * represents a html tag that has been closed and has a corresponding open
648     * tag on the stack.  Any tags, except single tags, that were opened
649     * (pushed on the stack) after the token are missing a close.
650     *
651     * @param htmlStack the stack of opened HTML tags.
652     * @param token the current HTML tag name that has been closed.
653     */
654    private void checkUnclosedTags(Deque<HtmlTag> htmlStack, String token) {
655        final Deque<HtmlTag> unclosedTags = new ArrayDeque<>();
656        HtmlTag lastOpenTag = htmlStack.pop();
657        while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
658            // Find unclosed elements. Put them on a stack so the
659            // output order won't be back-to-front.
660            if (isSingleTag(lastOpenTag)) {
661                lastOpenTag = htmlStack.pop();
662            }
663            else {
664                unclosedTags.push(lastOpenTag);
665                lastOpenTag = htmlStack.pop();
666            }
667        }
668
669        // Output the unterminated tags, if any
670        // Skip multiples, like <b>..<b>
671        String lastFound = "";
672        for (final HtmlTag htag : unclosedTags) {
673            lastOpenTag = htag;
674            if (lastOpenTag.getId().equals(lastFound)) {
675                continue;
676            }
677            lastFound = lastOpenTag.getId();
678            log(lastOpenTag.getLineNo(),
679                lastOpenTag.getPosition(),
680                MSG_UNCLOSED_HTML,
681                lastOpenTag.getText());
682        }
683    }
684
685    /**
686     * Determines if the HtmlTag is one which does not require a close tag.
687     *
688     * @param tag the HtmlTag to check.
689     * @return {@code true} if the HtmlTag is a single tag.
690     */
691    private static boolean isSingleTag(HtmlTag tag) {
692        // If it's a singleton tag (<p>, <br>, etc.), ignore it
693        // Can't simply not put them on the stack, since singletons
694        // like <dt> and <dd> (unhappily) may either be terminated
695        // or not terminated. Both options are legal.
696        return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
697    }
698
699    /**
700     * Determines if the HtmlTag is one which is allowed in a javadoc.
701     *
702     * @param tag the HtmlTag to check.
703     * @return {@code true} if the HtmlTag is an allowed html tag.
704     */
705    private static boolean isAllowedTag(HtmlTag tag) {
706        return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
707    }
708
709    /**
710     * Determines if the given token is an extra HTML tag. This indicates that
711     * a close tag was found that does not have a corresponding open tag.
712     *
713     * @param token an HTML tag id for which a close was found.
714     * @param htmlStack a Stack of previous open HTML tags.
715     * @return {@code false} if a previous open tag was found
716     *         for the token.
717     */
718    private static boolean isExtraHtml(String token, Deque<HtmlTag> htmlStack) {
719        boolean isExtra = true;
720        for (final HtmlTag tag : htmlStack) {
721            // Loop, looking for tags that are closed.
722            // The loop is needed in case there are unclosed
723            // tags on the stack. In that case, the stack would
724            // not be empty, but this tag would still be extra.
725            if (token.equalsIgnoreCase(tag.getId())) {
726                isExtra = false;
727                break;
728            }
729        }
730
731        return isExtra;
732    }
733
734    /**
735     * Setter to specify the visibility scope where Javadoc comments are checked.
736     *
737     * @param scope a scope.
738     */
739    public void setScope(Scope scope) {
740        this.scope = scope;
741    }
742
743    /**
744     * Setter to specify the visibility scope where Javadoc comments are not checked.
745     *
746     * @param excludeScope a scope.
747     */
748    public void setExcludeScope(Scope excludeScope) {
749        this.excludeScope = excludeScope;
750    }
751
752    /**
753     * Setter to specify the format for matching the end of a sentence.
754     *
755     * @param pattern a pattern.
756     */
757    public void setEndOfSentenceFormat(Pattern pattern) {
758        endOfSentenceFormat = pattern;
759    }
760
761    /**
762     * Setter to control whether to check the first sentence for proper end of sentence.
763     *
764     * @param flag {@code true} if the first sentence is to be checked
765     */
766    public void setCheckFirstSentence(boolean flag) {
767        checkFirstSentence = flag;
768    }
769
770    /**
771     * Setter to control whether to check for incomplete HTML tags.
772     *
773     * @param flag {@code true} if HTML checking is to be performed.
774     */
775    public void setCheckHtml(boolean flag) {
776        checkHtml = flag;
777    }
778
779    /**
780     * Setter to control whether to check if the Javadoc is missing a describing text.
781     *
782     * @param flag {@code true} if empty Javadoc checking should be done.
783     */
784    public void setCheckEmptyJavadoc(boolean flag) {
785        checkEmptyJavadoc = flag;
786    }
787
788}