001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024import java.util.List;
025
026import org.antlr.v4.runtime.BailErrorStrategy;
027import org.antlr.v4.runtime.BaseErrorListener;
028import org.antlr.v4.runtime.BufferedTokenStream;
029import org.antlr.v4.runtime.CharStreams;
030import org.antlr.v4.runtime.CommonToken;
031import org.antlr.v4.runtime.CommonTokenStream;
032import org.antlr.v4.runtime.FailedPredicateException;
033import org.antlr.v4.runtime.InputMismatchException;
034import org.antlr.v4.runtime.NoViableAltException;
035import org.antlr.v4.runtime.Parser;
036import org.antlr.v4.runtime.ParserRuleContext;
037import org.antlr.v4.runtime.RecognitionException;
038import org.antlr.v4.runtime.Recognizer;
039import org.antlr.v4.runtime.Token;
040import org.antlr.v4.runtime.misc.Interval;
041import org.antlr.v4.runtime.misc.ParseCancellationException;
042import org.antlr.v4.runtime.tree.ParseTree;
043import org.antlr.v4.runtime.tree.TerminalNode;
044
045import com.puppycrawl.tools.checkstyle.api.DetailAST;
046import com.puppycrawl.tools.checkstyle.api.DetailNode;
047import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
048import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
049import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocLexer;
050import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocParser;
051import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
052
053/**
054 * Used for parsing Javadoc comment as DetailNode tree.
055 *
056 */
057public class JavadocDetailNodeParser {
058
059    /**
060     * Message key of error message. Missed close HTML tag breaks structure
061     * of parse tree, so parser stops parsing and generates such error
062     * message. This case is special because parser prints error like
063     * {@code "no viable alternative at input 'b \n *\n'"} and it is not
064     * clear that error is about missed close HTML tag.
065     */
066    public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
067
068    /**
069     * Message key of error message.
070     */
071    public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
072        "javadoc.wrong.singleton.html.tag";
073
074    /**
075     * Parse error while rule recognition.
076     */
077    public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
078
079    /**
080     * Message property key for the Unclosed HTML message.
081     */
082    public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
083
084    /** Symbols with which javadoc starts. */
085    private static final String JAVADOC_START = "/**";
086
087    /**
088     * Line number of the Block comment AST that is being parsed.
089     */
090    private int blockCommentLineNumber;
091
092    /**
093     * Custom error listener.
094     */
095    private DescriptiveErrorListener errorListener;
096
097    /**
098     * Parses Javadoc comment as DetailNode tree.
099     * @param javadocCommentAst
100     *        DetailAST of Javadoc comment
101     * @return DetailNode tree of Javadoc comment
102     */
103    public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
104        blockCommentLineNumber = javadocCommentAst.getLineNo();
105
106        final String javadocComment = JavadocUtil.getJavadocCommentContent(javadocCommentAst);
107
108        // Use a new error listener each time to be able to use
109        // one check instance for multiple files to be checked
110        // without getting side effects.
111        errorListener = new DescriptiveErrorListener();
112
113        // Log messages should have line number in scope of file,
114        // not in scope of Javadoc comment.
115        // Offset is line number of beginning of Javadoc comment.
116        errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
117
118        final ParseStatus result = new ParseStatus();
119
120        try {
121            final JavadocParser javadocParser = createJavadocParser(javadocComment);
122
123            final ParseTree javadocParseTree = javadocParser.javadoc();
124
125            final DetailNode tree = convertParseTreeToDetailNode(javadocParseTree);
126            // adjust first line to indent of /**
127            adjustFirstLineToJavadocIndent(tree,
128                        javadocCommentAst.getColumnNo()
129                                + JAVADOC_START.length());
130            result.setTree(tree);
131            result.firstNonTightHtmlTag = getFirstNonTightHtmlTag(javadocParser);
132        }
133        catch (ParseCancellationException | IllegalArgumentException ex) {
134            ParseErrorMessage parseErrorMessage = null;
135
136            if (ex.getCause() instanceof FailedPredicateException
137                    || ex.getCause() instanceof NoViableAltException) {
138                final RecognitionException recognitionEx = (RecognitionException) ex.getCause();
139                if (recognitionEx.getCtx() instanceof JavadocParser.HtmlTagContext) {
140                    final Token htmlTagNameStart = getMissedHtmlTag(recognitionEx);
141                    parseErrorMessage = new ParseErrorMessage(
142                            errorListener.offset + htmlTagNameStart.getLine(),
143                            MSG_JAVADOC_MISSED_HTML_CLOSE,
144                            htmlTagNameStart.getCharPositionInLine(),
145                            htmlTagNameStart.getText());
146                }
147            }
148
149            if (parseErrorMessage == null) {
150                // If syntax error occurs then message is printed by error listener
151                // and parser throws this runtime exception to stop parsing.
152                // Just stop processing current Javadoc comment.
153                parseErrorMessage = errorListener.getErrorMessage();
154            }
155
156            result.setParseErrorMessage(parseErrorMessage);
157        }
158
159        return result;
160    }
161
162    /**
163     * Parses block comment content as javadoc comment.
164     * @param blockComment
165     *        block comment content.
166     * @return parse tree
167     */
168    private JavadocParser createJavadocParser(String blockComment) {
169        final JavadocLexer lexer = new JavadocLexer(CharStreams.fromString(blockComment));
170
171        final CommonTokenStream tokens = new CommonTokenStream(lexer);
172
173        final JavadocParser parser = new JavadocParser(tokens);
174
175        // remove default error listeners
176        parser.removeErrorListeners();
177
178        // add custom error listener that logs syntax errors
179        parser.addErrorListener(errorListener);
180
181        // JavadocParserErrorStrategy stops parsing on first parse error encountered unlike the
182        // DefaultErrorStrategy used by ANTLR which rather attempts error recovery.
183        parser.setErrorHandler(new JavadocParserErrorStrategy());
184
185        return parser;
186    }
187
188    /**
189     * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree.
190     *
191     * @param parseTreeNode root node of ParseTree
192     * @return root of DetailNode tree
193     * @noinspection SuspiciousArrayCast
194     */
195    private DetailNode convertParseTreeToDetailNode(ParseTree parseTreeNode) {
196        final JavadocNodeImpl rootJavadocNode = createRootJavadocNode(parseTreeNode);
197
198        JavadocNodeImpl currentJavadocParent = rootJavadocNode;
199        ParseTree parseTreeParent = parseTreeNode;
200
201        while (currentJavadocParent != null) {
202            // remove unnecessary children tokens
203            if (currentJavadocParent.getType() == JavadocTokenTypes.TEXT) {
204                currentJavadocParent.setChildren(JavadocNodeImpl.EMPTY_DETAIL_NODE_ARRAY);
205            }
206
207            final JavadocNodeImpl[] children =
208                    (JavadocNodeImpl[]) currentJavadocParent.getChildren();
209
210            insertChildrenNodes(children, parseTreeParent);
211
212            if (children.length > 0) {
213                currentJavadocParent = children[0];
214                parseTreeParent = parseTreeParent.getChild(0);
215            }
216            else {
217                JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtil
218                        .getNextSibling(currentJavadocParent);
219
220                ParseTree nextParseTreeSibling = getNextSibling(parseTreeParent);
221
222                while (nextJavadocSibling == null) {
223                    currentJavadocParent =
224                            (JavadocNodeImpl) currentJavadocParent.getParent();
225
226                    parseTreeParent = parseTreeParent.getParent();
227
228                    if (currentJavadocParent == null) {
229                        break;
230                    }
231
232                    nextJavadocSibling = (JavadocNodeImpl) JavadocUtil
233                            .getNextSibling(currentJavadocParent);
234
235                    nextParseTreeSibling = getNextSibling(parseTreeParent);
236                }
237                currentJavadocParent = nextJavadocSibling;
238                parseTreeParent = nextParseTreeSibling;
239            }
240        }
241
242        return rootJavadocNode;
243    }
244
245    /**
246     * Creates child nodes for each node from 'nodes' array.
247     * @param parseTreeParent original ParseTree parent node
248     * @param nodes array of JavadocNodeImpl nodes
249     */
250    private void insertChildrenNodes(final JavadocNodeImpl[] nodes, ParseTree parseTreeParent) {
251        for (int i = 0; i < nodes.length; i++) {
252            final JavadocNodeImpl currentJavadocNode = nodes[i];
253            final ParseTree currentParseTreeNodeChild = parseTreeParent.getChild(i);
254            final JavadocNodeImpl[] subChildren =
255                    createChildrenNodes(currentJavadocNode, currentParseTreeNodeChild);
256            currentJavadocNode.setChildren(subChildren);
257        }
258    }
259
260    /**
261     * Creates children Javadoc nodes base on ParseTree node's children.
262     * @param parentJavadocNode node that will be parent for created children
263     * @param parseTreeNode original ParseTree node
264     * @return array of Javadoc nodes
265     */
266    private JavadocNodeImpl[]
267            createChildrenNodes(JavadocNodeImpl parentJavadocNode, ParseTree parseTreeNode) {
268        final JavadocNodeImpl[] children =
269                new JavadocNodeImpl[parseTreeNode.getChildCount()];
270
271        for (int j = 0; j < children.length; j++) {
272            final JavadocNodeImpl child =
273                    createJavadocNode(parseTreeNode.getChild(j), parentJavadocNode, j);
274
275            children[j] = child;
276        }
277        return children;
278    }
279
280    /**
281     * Creates root JavadocNodeImpl node base on ParseTree root node.
282     * @param parseTreeNode ParseTree root node
283     * @return root Javadoc node
284     */
285    private JavadocNodeImpl createRootJavadocNode(ParseTree parseTreeNode) {
286        final JavadocNodeImpl rootJavadocNode = createJavadocNode(parseTreeNode, null, -1);
287
288        final int childCount = parseTreeNode.getChildCount();
289        final DetailNode[] children = rootJavadocNode.getChildren();
290
291        for (int i = 0; i < childCount; i++) {
292            final JavadocNodeImpl child = createJavadocNode(parseTreeNode.getChild(i),
293                    rootJavadocNode, i);
294            children[i] = child;
295        }
296        rootJavadocNode.setChildren(children);
297        return rootJavadocNode;
298    }
299
300    /**
301     * Creates JavadocNodeImpl node on base of ParseTree node.
302     *
303     * @param parseTree ParseTree node
304     * @param parent DetailNode that will be parent of new node
305     * @param index child index that has new node
306     * @return JavadocNodeImpl node on base of ParseTree node.
307     */
308    private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) {
309        final JavadocNodeImpl node = new JavadocNodeImpl();
310        if (parseTree.getChildCount() == 0
311                || "Text".equals(getNodeClassNameWithoutContext(parseTree))) {
312            node.setText(parseTree.getText());
313        }
314        else {
315            node.setText(getFormattedNodeClassNameWithoutContext(parseTree));
316        }
317        node.setColumnNumber(getColumn(parseTree));
318        node.setLineNumber(getLine(parseTree) + blockCommentLineNumber);
319        node.setIndex(index);
320        node.setType(getTokenType(parseTree));
321        node.setParent(parent);
322        node.setChildren(new JavadocNodeImpl[parseTree.getChildCount()]);
323        return node;
324    }
325
326    /**
327     * Adjust first line nodes to javadoc indent.
328     * @param tree DetailNode tree root
329     * @param javadocColumnNumber javadoc indent
330     */
331    private void adjustFirstLineToJavadocIndent(DetailNode tree, int javadocColumnNumber) {
332        if (tree.getLineNumber() == blockCommentLineNumber) {
333            ((JavadocNodeImpl) tree).setColumnNumber(tree.getColumnNumber() + javadocColumnNumber);
334            final DetailNode[] children = tree.getChildren();
335            for (DetailNode child : children) {
336                adjustFirstLineToJavadocIndent(child, javadocColumnNumber);
337            }
338        }
339    }
340
341    /**
342     * Gets line number from ParseTree node.
343     * @param tree
344     *        ParseTree node
345     * @return line number
346     */
347    private static int getLine(ParseTree tree) {
348        final int line;
349        if (tree instanceof TerminalNode) {
350            line = ((TerminalNode) tree).getSymbol().getLine() - 1;
351        }
352        else {
353            final ParserRuleContext rule = (ParserRuleContext) tree;
354            line = rule.start.getLine() - 1;
355        }
356        return line;
357    }
358
359    /**
360     * Gets column number from ParseTree node.
361     * @param tree
362     *        ParseTree node
363     * @return column number
364     */
365    private static int getColumn(ParseTree tree) {
366        final int column;
367        if (tree instanceof TerminalNode) {
368            column = ((TerminalNode) tree).getSymbol().getCharPositionInLine();
369        }
370        else {
371            final ParserRuleContext rule = (ParserRuleContext) tree;
372            column = rule.start.getCharPositionInLine();
373        }
374        return column;
375    }
376
377    /**
378     * Gets next sibling of ParseTree node.
379     * @param node ParseTree node
380     * @return next sibling of ParseTree node.
381     */
382    private static ParseTree getNextSibling(ParseTree node) {
383        ParseTree nextSibling = null;
384
385        if (node.getParent() != null) {
386            final ParseTree parent = node.getParent();
387            int index = 0;
388            while (true) {
389                final ParseTree currentNode = parent.getChild(index);
390                if (currentNode.equals(node)) {
391                    nextSibling = parent.getChild(index + 1);
392                    break;
393                }
394                index++;
395            }
396        }
397        return nextSibling;
398    }
399
400    /**
401     * Gets token type of ParseTree node from JavadocTokenTypes class.
402     * @param node ParseTree node.
403     * @return token type from JavadocTokenTypes
404     */
405    private static int getTokenType(ParseTree node) {
406        final int tokenType;
407
408        if (node.getChildCount() == 0) {
409            tokenType = ((TerminalNode) node).getSymbol().getType();
410        }
411        else {
412            final String className = getNodeClassNameWithoutContext(node);
413            tokenType = JavadocUtil.getTokenId(convertUpperCamelToUpperUnderscore(className));
414        }
415
416        return tokenType;
417    }
418
419    /**
420     * Gets class name of ParseTree node and removes 'Context' postfix at the
421     * end and formats it.
422     * @param node {@code ParseTree} node whose class name is to be formatted and returned
423     * @return uppercased class name without the word 'Context' and with appropriately
424     *     inserted underscores
425     */
426    private static String getFormattedNodeClassNameWithoutContext(ParseTree node) {
427        final String classNameWithoutContext = getNodeClassNameWithoutContext(node);
428        return convertUpperCamelToUpperUnderscore(classNameWithoutContext);
429    }
430
431    /**
432     * Gets class name of ParseTree node and removes 'Context' postfix at the
433     * end.
434     * @param node
435     *        ParseTree node.
436     * @return class name without 'Context'
437     */
438    private static String getNodeClassNameWithoutContext(ParseTree node) {
439        final String className = node.getClass().getSimpleName();
440        // remove 'Context' at the end
441        final int contextLength = 7;
442        return className.substring(0, className.length() - contextLength);
443    }
444
445    /**
446     * Method to get the missed HTML tag to generate more informative error message for the user.
447     * This method doesn't concern itself with
448     * <a href="https://www.w3.org/TR/html51/syntax.html#void-elements">void elements</a>
449     * since it is forbidden to close them.
450     * Missed HTML tags for the following tags will <i>not</i> generate an error message from ANTLR:
451     * {@code
452     * <p>
453     * <li>
454     * <tr>
455     * <td>
456     * <th>
457     * <body>
458     * <colgroup>
459     * <dd>
460     * <dt>
461     * <head>
462     * <html>
463     * <option>
464     * <tbody>
465     * <thead>
466     * <tfoot>
467     * }
468     * @param exception {@code NoViableAltException} object catched while parsing javadoc
469     * @return returns appropriate {@link Token} if a HTML close tag is missed;
470     *     null otherwise
471     */
472    private static Token getMissedHtmlTag(RecognitionException exception) {
473        Token htmlTagNameStart = null;
474        final Interval sourceInterval = exception.getCtx().getSourceInterval();
475        final List<Token> tokenList = ((BufferedTokenStream) exception.getInputStream())
476                .getTokens(sourceInterval.a, sourceInterval.b);
477        final Deque<Token> stack = new ArrayDeque<>();
478        int prevTokenType = JavadocTokenTypes.EOF;
479        for (final Token token : tokenList) {
480            final int tokenType = token.getType();
481            if (tokenType == JavadocTokenTypes.HTML_TAG_NAME
482                    && prevTokenType == JavadocTokenTypes.START) {
483                stack.push(token);
484            }
485            else if (tokenType == JavadocTokenTypes.HTML_TAG_NAME && !stack.isEmpty()) {
486                if (stack.peek().getText().equals(token.getText())) {
487                    stack.pop();
488                }
489                else {
490                    htmlTagNameStart = stack.pop();
491                }
492            }
493            prevTokenType = tokenType;
494        }
495        if (htmlTagNameStart == null) {
496            htmlTagNameStart = stack.pop();
497        }
498        return htmlTagNameStart;
499    }
500
501    /**
502     * This method is used to get the first non-tight HTML tag encountered while parsing javadoc.
503     * This shall eventually be reflected by the {@link ParseStatus} object returned by
504     * {@link #parseJavadocAsDetailNode(DetailAST)} method via the instance member
505     * {@link ParseStatus#firstNonTightHtmlTag}, and checks not supposed to process non-tight HTML
506     * or the ones which are supposed to log violation for non-tight javadocs can utilize that.
507     *
508     * @param javadocParser The ANTLR recognizer instance which has been used to parse the javadoc
509     * @return First non-tight HTML tag if one exists; null otherwise
510     */
511    private Token getFirstNonTightHtmlTag(JavadocParser javadocParser) {
512        final CommonToken offendingToken;
513        final ParserRuleContext nonTightTagStartContext = javadocParser.nonTightTagStartContext;
514        if (nonTightTagStartContext == null) {
515            offendingToken = null;
516        }
517        else {
518            final Token token = ((TerminalNode) nonTightTagStartContext.getChild(1))
519                    .getSymbol();
520            offendingToken = new CommonToken(token);
521            offendingToken.setLine(offendingToken.getLine() + errorListener.offset);
522        }
523        return offendingToken;
524    }
525
526    /**
527     * Converts the given {@code text} from camel case to all upper case with
528     * underscores separating each word.
529     * @param text The string to convert.
530     * @return The result of the conversion.
531     */
532    private static String convertUpperCamelToUpperUnderscore(String text) {
533        final StringBuilder result = new StringBuilder(20);
534        boolean first = true;
535        for (char letter : text.toCharArray()) {
536            if (!first && Character.isUpperCase(letter)) {
537                result.append('_');
538            }
539            result.append(Character.toUpperCase(letter));
540            first = false;
541        }
542        return result.toString();
543    }
544
545    /**
546     * Custom error listener for JavadocParser that prints user readable errors.
547     */
548    private static class DescriptiveErrorListener extends BaseErrorListener {
549
550        /**
551         * Offset is line number of beginning of the Javadoc comment. Log
552         * messages should have line number in scope of file, not in scope of
553         * Javadoc comment.
554         */
555        private int offset;
556
557        /**
558         * Error message that appeared while parsing.
559         */
560        private ParseErrorMessage errorMessage;
561
562        /**
563         * Getter for error message during parsing.
564         * @return Error message during parsing.
565         */
566        private ParseErrorMessage getErrorMessage() {
567            return errorMessage;
568        }
569
570        /**
571         * Sets offset. Offset is line number of beginning of the Javadoc
572         * comment. Log messages should have line number in scope of file, not
573         * in scope of Javadoc comment.
574         * @param offset
575         *        offset line number
576         */
577        public void setOffset(int offset) {
578            this.offset = offset;
579        }
580
581        /**
582         * Logs parser errors in Checkstyle manner. Parser can generate error
583         * messages. There is special error that parser can generate. It is
584         * missed close HTML tag. This case is special because parser prints
585         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
586         * is not clear that error is about missed close HTML tag. Other error
587         * messages are not special and logged simply as "Parse Error...".
588         *
589         * <p>{@inheritDoc}
590         */
591        @Override
592        public void syntaxError(
593                Recognizer<?, ?> recognizer, Object offendingSymbol,
594                int line, int charPositionInLine,
595                String msg, RecognitionException ex) {
596            final int lineNumber = offset + line;
597
598            if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
599                errorMessage = new ParseErrorMessage(lineNumber,
600                        MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine,
601                        ((Token) offendingSymbol).getText());
602
603                throw new IllegalArgumentException(msg);
604            }
605
606            final int ruleIndex = ex.getCtx().getRuleIndex();
607            final String ruleName = recognizer.getRuleNames()[ruleIndex];
608            final String upperCaseRuleName = convertUpperCamelToUpperUnderscore(ruleName);
609
610            errorMessage = new ParseErrorMessage(lineNumber,
611                    MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
612
613        }
614
615    }
616
617    /**
618     * Contains result of parsing javadoc comment: DetailNode tree and parse
619     * error message.
620     */
621    public static class ParseStatus {
622
623        /**
624         * DetailNode tree (is null if parsing fails).
625         */
626        private DetailNode tree;
627
628        /**
629         * Parse error message (is null if parsing is successful).
630         */
631        private ParseErrorMessage parseErrorMessage;
632
633        /**
634         * Stores the first non-tight HTML tag encountered while parsing javadoc.
635         *
636         * @see <a
637         *     href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
638         *     Tight HTML rules</a>
639         */
640        private Token firstNonTightHtmlTag;
641
642        /**
643         * Getter for DetailNode tree.
644         * @return DetailNode tree if parsing was successful, null otherwise.
645         */
646        public DetailNode getTree() {
647            return tree;
648        }
649
650        /**
651         * Sets DetailNode tree.
652         * @param tree DetailNode tree.
653         */
654        public void setTree(DetailNode tree) {
655            this.tree = tree;
656        }
657
658        /**
659         * Getter for error message during parsing.
660         * @return Error message if parsing was unsuccessful, null otherwise.
661         */
662        public ParseErrorMessage getParseErrorMessage() {
663            return parseErrorMessage;
664        }
665
666        /**
667         * Sets parse error message.
668         * @param parseErrorMessage Parse error message.
669         */
670        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
671            this.parseErrorMessage = parseErrorMessage;
672        }
673
674        /**
675         * This method is used to check if the javadoc parsed has non-tight HTML tags.
676         *
677         * @return returns true if the javadoc has at least one non-tight HTML tag; false otherwise
678         * @see <a
679         *     href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
680         *     Tight HTML rules</a>
681         */
682        public boolean isNonTight() {
683            return firstNonTightHtmlTag != null;
684        }
685
686        /**
687         * Getter for the first non-tight HTML tag encountered while parsing javadoc.
688         *
689         * @return the first non-tight HTML tag that is encountered while parsing Javadoc,
690         *     if one exists
691         * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
692         *     Tight HTML rules</a>
693         */
694        public Token getFirstNonTightHtmlTag() {
695            return firstNonTightHtmlTag;
696        }
697
698    }
699
700    /**
701     * Contains information about parse error message.
702     */
703    public static class ParseErrorMessage {
704
705        /**
706         * Line number where parse error occurred.
707         */
708        private final int lineNumber;
709
710        /**
711         * Key for error message.
712         */
713        private final String messageKey;
714
715        /**
716         * Error message arguments.
717         */
718        private final Object[] messageArguments;
719
720        /**
721         * Initializes parse error message.
722         *
723         * @param lineNumber line number
724         * @param messageKey message key
725         * @param messageArguments message arguments
726         */
727        /* package */ ParseErrorMessage(int lineNumber, String messageKey,
728                Object... messageArguments) {
729            this.lineNumber = lineNumber;
730            this.messageKey = messageKey;
731            this.messageArguments = messageArguments.clone();
732        }
733
734        /**
735         * Getter for line number where parse error occurred.
736         * @return Line number where parse error occurred.
737         */
738        public int getLineNumber() {
739            return lineNumber;
740        }
741
742        /**
743         * Getter for key for error message.
744         * @return Key for error message.
745         */
746        public String getMessageKey() {
747            return messageKey;
748        }
749
750        /**
751         * Getter for error message arguments.
752         * @return Array of error message arguments.
753         */
754        public Object[] getMessageArguments() {
755            return messageArguments.clone();
756        }
757
758    }
759
760    /**
761     * The DefaultErrorStrategy used by ANTLR attempts to recover from parse errors
762     * which might result in a performance overhead. Also, a parse error indicate
763     * that javadoc doesn't follow checkstyle Javadoc grammar and the user should be made aware
764     * of it.
765     * <a href="https://www.antlr.org/api/Java/org/antlr/v4/runtime/BailErrorStrategy.html">
766     * BailErrorStrategy</a> is used to make ANTLR generated parser bail out on the first error
767     * in parser and not attempt any recovery methods but it doesn't report error to the
768     * listeners. This class is to ensure proper error reporting.
769     *
770     * @see DescriptiveErrorListener
771     * @see <a href="https://www.antlr.org/api/Java/org/antlr/v4/runtime/ANTLRErrorStrategy.html">
772     *     ANTLRErrorStrategy</a>
773     */
774    private static class JavadocParserErrorStrategy extends BailErrorStrategy {
775
776        @Override
777        public Token recoverInline(Parser recognizer) {
778            reportError(recognizer, new InputMismatchException(recognizer));
779            return super.recoverInline(recognizer);
780        }
781
782    }
783
784}