001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 int[]}.
129 * Default value is:
130 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
131 * ANNOTATION_DEF</a>,
132 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
133 * ANNOTATION_FIELD_DEF</a>,
134 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
135 * CLASS_DEF</a>,
136 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
137 * CTOR_DEF</a>,
138 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_CONSTANT_DEF">
139 * ENUM_CONSTANT_DEF</a>,
140 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
141 * ENUM_DEF</a>,
142 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
143 * INTERFACE_DEF</a>,
144 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
145 * METHOD_DEF</a>,
146 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PACKAGE_DEF">
147 * PACKAGE_DEF</a>,
148 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#VARIABLE_DEF">
149 * VARIABLE_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.missing}
291 * </li>
292 * <li>
293 * {@code javadoc.noPeriod}
294 * </li>
295 * <li>
296 * {@code javadoc.unclosedHtml}
297 * </li>
298 * </ul>
299 *
300 * @since 3.2
301 */
302@StatelessCheck
303public class JavadocStyleCheck
304    extends AbstractCheck {
305
306    /** Message property key for the Missing Javadoc message. */
307    public static final String MSG_JAVADOC_MISSING = "javadoc.missing";
308
309    /** Message property key for the Empty Javadoc message. */
310    public static final String MSG_EMPTY = "javadoc.empty";
311
312    /** Message property key for the No Javadoc end of Sentence Period message. */
313    public static final String MSG_NO_PERIOD = "javadoc.noPeriod";
314
315    /** Message property key for the Incomplete Tag message. */
316    public static final String MSG_INCOMPLETE_TAG = "javadoc.incompleteTag";
317
318    /** Message property key for the Unclosed HTML message. */
319    public static final String MSG_UNCLOSED_HTML = JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
320
321    /** Message property key for the Extra HTML message. */
322    public static final String MSG_EXTRA_HTML = "javadoc.extraHtml";
323
324    /** HTML tags that do not require a close tag. */
325    private static final Set<String> SINGLE_TAGS = Collections.unmodifiableSortedSet(
326        Arrays.stream(new String[] {"br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th", })
327            .collect(Collectors.toCollection(TreeSet::new)));
328
329    /**
330     * HTML tags that are allowed in java docs.
331     * From https://www.w3schools.com/tags/default.asp
332     * The forms and structure tags are not allowed
333     */
334    private static final Set<String> ALLOWED_TAGS = Collections.unmodifiableSortedSet(
335        Arrays.stream(new String[] {
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        .collect(Collectors.toCollection(TreeSet::new)));
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        };
388    }
389
390    @Override
391    public int[] getRequiredTokens() {
392        return CommonUtil.EMPTY_INT_ARRAY;
393    }
394
395    @Override
396    public void visitToken(DetailAST ast) {
397        if (shouldCheck(ast)) {
398            final FileContents contents = getFileContents();
399            // Need to start searching for the comment before the annotations
400            // that may exist. Even if annotations are not defined on the
401            // package, the ANNOTATIONS AST is defined.
402            final TextBlock textBlock =
403                contents.getJavadocBefore(ast.getFirstChild().getLineNo());
404
405            checkComment(ast, textBlock);
406        }
407    }
408
409    /**
410     * Whether we should check this node.
411     *
412     * @param ast a given node.
413     * @return whether we should check a given node.
414     */
415    private boolean shouldCheck(final DetailAST ast) {
416        boolean check = false;
417
418        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
419            check = getFileContents().inPackageInfo();
420        }
421        else if (!ScopeUtil.isInCodeBlock(ast)) {
422            final Scope customScope;
423
424            if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)
425                    || ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
426                customScope = Scope.PUBLIC;
427            }
428            else {
429                customScope = ScopeUtil.getScopeFromMods(ast.findFirstToken(TokenTypes.MODIFIERS));
430            }
431            final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
432
433            check = customScope.isIn(scope)
434                    && (surroundingScope == null || surroundingScope.isIn(scope))
435                    && (excludeScope == null
436                        || !customScope.isIn(excludeScope)
437                        || surroundingScope != null
438                            && !surroundingScope.isIn(excludeScope));
439        }
440        return check;
441    }
442
443    /**
444     * Performs the various checks against the Javadoc comment.
445     *
446     * @param ast the AST of the element being documented
447     * @param comment the source lines that make up the Javadoc comment.
448     *
449     * @see #checkFirstSentenceEnding(DetailAST, TextBlock)
450     * @see #checkHtmlTags(DetailAST, TextBlock)
451     */
452    private void checkComment(final DetailAST ast, final TextBlock comment) {
453        if (comment == null) {
454            // checking for missing docs in JavadocStyleCheck is not consistent
455            // with the rest of CheckStyle...  Even though, I didn't think it
456            // made sense to make another check just to ensure that the
457            // package-info.java file actually contains package Javadocs.
458            if (getFileContents().inPackageInfo()) {
459                log(ast, MSG_JAVADOC_MISSING);
460            }
461        }
462        else {
463            if (checkFirstSentence) {
464                checkFirstSentenceEnding(ast, comment);
465            }
466
467            if (checkHtml) {
468                checkHtmlTags(ast, comment);
469            }
470
471            if (checkEmptyJavadoc) {
472                checkJavadocIsNotEmpty(comment);
473            }
474        }
475    }
476
477    /**
478     * Checks that the first sentence ends with proper punctuation.  This method
479     * uses a regular expression that checks for the presence of a period,
480     * question mark, or exclamation mark followed either by whitespace, an
481     * HTML element, or the end of string. This method ignores {_AT_inheritDoc}
482     * comments for TokenTypes that are valid for {_AT_inheritDoc}.
483     *
484     * @param ast the current node
485     * @param comment the source lines that make up the Javadoc comment.
486     */
487    private void checkFirstSentenceEnding(final DetailAST ast, TextBlock comment) {
488        final String commentText = getCommentText(comment.getText());
489
490        if (!commentText.isEmpty()
491            && !endOfSentenceFormat.matcher(commentText).find()
492            && !(commentText.startsWith("{@inheritDoc}")
493            && JavadocTagInfo.INHERIT_DOC.isValidOn(ast))) {
494            log(comment.getStartLineNo(), MSG_NO_PERIOD);
495        }
496    }
497
498    /**
499     * Checks that the Javadoc is not empty.
500     *
501     * @param comment the source lines that make up the Javadoc comment.
502     */
503    private void checkJavadocIsNotEmpty(TextBlock comment) {
504        final String commentText = getCommentText(comment.getText());
505
506        if (commentText.isEmpty()) {
507            log(comment.getStartLineNo(), MSG_EMPTY);
508        }
509    }
510
511    /**
512     * Returns the comment text from the Javadoc.
513     *
514     * @param comments the lines of Javadoc.
515     * @return a comment text String.
516     */
517    private static String getCommentText(String... comments) {
518        final StringBuilder builder = new StringBuilder(1024);
519        for (final String line : comments) {
520            final int textStart = findTextStart(line);
521
522            if (textStart != -1) {
523                if (line.charAt(textStart) == '@') {
524                    // we have found the tag section
525                    break;
526                }
527                builder.append(line.substring(textStart));
528                trimTail(builder);
529                builder.append('\n');
530            }
531        }
532
533        return builder.toString().trim();
534    }
535
536    /**
537     * Finds the index of the first non-whitespace character ignoring the
538     * Javadoc comment start and end strings (&#47** and *&#47) as well as any
539     * leading asterisk.
540     *
541     * @param line the Javadoc comment line of text to scan.
542     * @return the int index relative to 0 for the start of text
543     *         or -1 if not found.
544     */
545    private static int findTextStart(String line) {
546        int textStart = -1;
547        int index = 0;
548        while (index < line.length()) {
549            if (!Character.isWhitespace(line.charAt(index))) {
550                if (line.regionMatches(index, "/**", 0, "/**".length())) {
551                    index += 2;
552                }
553                else if (line.regionMatches(index, "*/", 0, 2)) {
554                    index++;
555                }
556                else if (line.charAt(index) != '*') {
557                    textStart = index;
558                    break;
559                }
560            }
561            index++;
562        }
563        return textStart;
564    }
565
566    /**
567     * Trims any trailing whitespace or the end of Javadoc comment string.
568     *
569     * @param builder the StringBuilder to trim.
570     */
571    private static void trimTail(StringBuilder builder) {
572        int index = builder.length() - 1;
573        while (true) {
574            if (Character.isWhitespace(builder.charAt(index))) {
575                builder.deleteCharAt(index);
576            }
577            else if (index > 0 && builder.charAt(index) == '/'
578                    && builder.charAt(index - 1) == '*') {
579                builder.deleteCharAt(index);
580                builder.deleteCharAt(index - 1);
581                index--;
582                while (builder.charAt(index - 1) == '*') {
583                    builder.deleteCharAt(index - 1);
584                    index--;
585                }
586            }
587            else {
588                break;
589            }
590            index--;
591        }
592    }
593
594    /**
595     * Checks the comment for HTML tags that do not have a corresponding close
596     * tag or a close tag that has no previous open tag.  This code was
597     * primarily copied from the DocCheck checkHtml method.
598     *
599     * @param ast the node with the Javadoc
600     * @param comment the {@code TextBlock} which represents
601     *                 the Javadoc comment.
602     * @noinspection MethodWithMultipleReturnPoints
603     */
604    // -@cs[ReturnCount] Too complex to break apart.
605    private void checkHtmlTags(final DetailAST ast, final TextBlock comment) {
606        final int lineNo = comment.getStartLineNo();
607        final Deque<HtmlTag> htmlStack = new ArrayDeque<>();
608        final String[] text = comment.getText();
609
610        final TagParser parser = new TagParser(text, lineNo);
611
612        while (parser.hasNextTag()) {
613            final HtmlTag tag = parser.nextTag();
614
615            if (tag.isIncompleteTag()) {
616                log(tag.getLineNo(), MSG_INCOMPLETE_TAG,
617                    text[tag.getLineNo() - lineNo]);
618                return;
619            }
620            if (tag.isClosedTag()) {
621                // do nothing
622                continue;
623            }
624            if (tag.isCloseTag()) {
625                // We have found a close tag.
626                if (isExtraHtml(tag.getId(), htmlStack)) {
627                    // No corresponding open tag was found on the stack.
628                    log(tag.getLineNo(),
629                        tag.getPosition(),
630                        MSG_EXTRA_HTML,
631                        tag.getText());
632                }
633                else {
634                    // See if there are any unclosed tags that were opened
635                    // after this one.
636                    checkUnclosedTags(htmlStack, tag.getId());
637                }
638            }
639            else {
640                // We only push html tags that are allowed
641                if (isAllowedTag(tag)) {
642                    htmlStack.push(tag);
643                }
644            }
645        }
646
647        // Identify any tags left on the stack.
648        // Skip multiples, like <b>...<b>
649        String lastFound = "";
650        final List<String> typeParameters = CheckUtil.getTypeParameterNames(ast);
651        for (final HtmlTag htmlTag : htmlStack) {
652            if (!isSingleTag(htmlTag)
653                && !htmlTag.getId().equals(lastFound)
654                && !typeParameters.contains(htmlTag.getId())) {
655                log(htmlTag.getLineNo(), htmlTag.getPosition(),
656                        MSG_UNCLOSED_HTML, htmlTag.getText());
657                lastFound = htmlTag.getId();
658            }
659        }
660    }
661
662    /**
663     * Checks to see if there are any unclosed tags on the stack.  The token
664     * represents a html tag that has been closed and has a corresponding open
665     * tag on the stack.  Any tags, except single tags, that were opened
666     * (pushed on the stack) after the token are missing a close.
667     *
668     * @param htmlStack the stack of opened HTML tags.
669     * @param token the current HTML tag name that has been closed.
670     */
671    private void checkUnclosedTags(Deque<HtmlTag> htmlStack, String token) {
672        final Deque<HtmlTag> unclosedTags = new ArrayDeque<>();
673        HtmlTag lastOpenTag = htmlStack.pop();
674        while (!token.equalsIgnoreCase(lastOpenTag.getId())) {
675            // Find unclosed elements. Put them on a stack so the
676            // output order won't be back-to-front.
677            if (isSingleTag(lastOpenTag)) {
678                lastOpenTag = htmlStack.pop();
679            }
680            else {
681                unclosedTags.push(lastOpenTag);
682                lastOpenTag = htmlStack.pop();
683            }
684        }
685
686        // Output the unterminated tags, if any
687        // Skip multiples, like <b>..<b>
688        String lastFound = "";
689        for (final HtmlTag htag : unclosedTags) {
690            lastOpenTag = htag;
691            if (lastOpenTag.getId().equals(lastFound)) {
692                continue;
693            }
694            lastFound = lastOpenTag.getId();
695            log(lastOpenTag.getLineNo(),
696                lastOpenTag.getPosition(),
697                MSG_UNCLOSED_HTML,
698                lastOpenTag.getText());
699        }
700    }
701
702    /**
703     * Determines if the HtmlTag is one which does not require a close tag.
704     *
705     * @param tag the HtmlTag to check.
706     * @return {@code true} if the HtmlTag is a single tag.
707     */
708    private static boolean isSingleTag(HtmlTag tag) {
709        // If its a singleton tag (<p>, <br>, etc.), ignore it
710        // Can't simply not put them on the stack, since singletons
711        // like <dt> and <dd> (unhappily) may either be terminated
712        // or not terminated. Both options are legal.
713        return SINGLE_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
714    }
715
716    /**
717     * Determines if the HtmlTag is one which is allowed in a javadoc.
718     *
719     * @param tag the HtmlTag to check.
720     * @return {@code true} if the HtmlTag is an allowed html tag.
721     */
722    private static boolean isAllowedTag(HtmlTag tag) {
723        return ALLOWED_TAGS.contains(tag.getId().toLowerCase(Locale.ENGLISH));
724    }
725
726    /**
727     * Determines if the given token is an extra HTML tag. This indicates that
728     * a close tag was found that does not have a corresponding open tag.
729     *
730     * @param token an HTML tag id for which a close was found.
731     * @param htmlStack a Stack of previous open HTML tags.
732     * @return {@code false} if a previous open tag was found
733     *         for the token.
734     */
735    private static boolean isExtraHtml(String token, Deque<HtmlTag> htmlStack) {
736        boolean isExtra = true;
737        for (final HtmlTag tag : htmlStack) {
738            // Loop, looking for tags that are closed.
739            // The loop is needed in case there are unclosed
740            // tags on the stack. In that case, the stack would
741            // not be empty, but this tag would still be extra.
742            if (token.equalsIgnoreCase(tag.getId())) {
743                isExtra = false;
744                break;
745            }
746        }
747
748        return isExtra;
749    }
750
751    /**
752     * Setter to specify the visibility scope where Javadoc comments are checked.
753     *
754     * @param scope a scope.
755     */
756    public void setScope(Scope scope) {
757        this.scope = scope;
758    }
759
760    /**
761     * Setter to specify the visibility scope where Javadoc comments are not checked.
762     *
763     * @param excludeScope a scope.
764     */
765    public void setExcludeScope(Scope excludeScope) {
766        this.excludeScope = excludeScope;
767    }
768
769    /**
770     * Setter to specify the format for matching the end of a sentence.
771     *
772     * @param pattern a pattern.
773     */
774    public void setEndOfSentenceFormat(Pattern pattern) {
775        endOfSentenceFormat = pattern;
776    }
777
778    /**
779     * Setter to control whether to check the first sentence for proper end of sentence.
780     *
781     * @param flag {@code true} if the first sentence is to be checked
782     */
783    public void setCheckFirstSentence(boolean flag) {
784        checkFirstSentence = flag;
785    }
786
787    /**
788     * Setter to control whether to check for incomplete HTML tags.
789     *
790     * @param flag {@code true} if HTML checking is to be performed.
791     */
792    public void setCheckHtml(boolean flag) {
793        checkHtml = flag;
794    }
795
796    /**
797     * Setter to control whether to check if the Javadoc is missing a describing text.
798     *
799     * @param flag {@code true} if empty Javadoc checking should be done.
800     */
801    public void setCheckEmptyJavadoc(boolean flag) {
802        checkEmptyJavadoc = flag;
803    }
804
805}