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