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