View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.List;
25  
26  import org.antlr.v4.runtime.BaseErrorListener;
27  import org.antlr.v4.runtime.BufferedTokenStream;
28  import org.antlr.v4.runtime.CharStreams;
29  import org.antlr.v4.runtime.CommonToken;
30  import org.antlr.v4.runtime.CommonTokenStream;
31  import org.antlr.v4.runtime.FailedPredicateException;
32  import org.antlr.v4.runtime.NoViableAltException;
33  import org.antlr.v4.runtime.ParserRuleContext;
34  import org.antlr.v4.runtime.RecognitionException;
35  import org.antlr.v4.runtime.Recognizer;
36  import org.antlr.v4.runtime.Token;
37  import org.antlr.v4.runtime.misc.Interval;
38  import org.antlr.v4.runtime.misc.ParseCancellationException;
39  import org.antlr.v4.runtime.tree.ParseTree;
40  import org.antlr.v4.runtime.tree.TerminalNode;
41  
42  import com.puppycrawl.tools.checkstyle.api.DetailAST;
43  import com.puppycrawl.tools.checkstyle.api.DetailNode;
44  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
45  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
46  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocLexer;
47  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocParser;
48  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
49  
50  /**
51   * Used for parsing Javadoc comment as DetailNode tree.
52   *
53   */
54  public class JavadocDetailNodeParser {
55  
56      /**
57       * Message key of error message. Missed close HTML tag breaks structure
58       * of parse tree, so parser stops parsing and generates such error
59       * message. This case is special because parser prints error like
60       * {@code "no viable alternative at input 'b \n *\n'"} and it is not
61       * clear that error is about missed close HTML tag.
62       */
63      public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
64  
65      /**
66       * Message key of error message.
67       */
68      public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
69          "javadoc.wrong.singleton.html.tag";
70  
71      /**
72       * Parse error while rule recognition.
73       */
74      public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
75  
76      /**
77       * Message property key for the Unclosed HTML message.
78       */
79      public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
80  
81      /** Symbols with which javadoc starts. */
82      private static final String JAVADOC_START = "/**";
83  
84      /**
85       * Line number of the Block comment AST that is being parsed.
86       */
87      private int blockCommentLineNumber;
88  
89      /**
90       * Parses Javadoc comment as DetailNode tree.
91       *
92       * @param javadocCommentAst
93       *        DetailAST of Javadoc comment
94       * @return DetailNode tree of Javadoc comment
95       */
96      public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
97          blockCommentLineNumber = javadocCommentAst.getLineNo();
98  
99          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 }