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