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