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