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