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