View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.atn.PredictionMode;
38  import org.antlr.v4.runtime.misc.Interval;
39  import org.antlr.v4.runtime.misc.ParseCancellationException;
40  import org.antlr.v4.runtime.tree.ParseTree;
41  import org.antlr.v4.runtime.tree.TerminalNode;
42  
43  import com.puppycrawl.tools.checkstyle.api.DetailAST;
44  import com.puppycrawl.tools.checkstyle.api.DetailNode;
45  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
46  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
47  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocLexer;
48  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocParser;
49  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
50  
51  /**
52   * Used for parsing Javadoc comment as DetailNode tree.
53   *
54   */
55  public class JavadocDetailNodeParser {
56  
57      /**
58       * Message key of error message. Missed close HTML tag breaks structure
59       * of parse tree, so parser stops parsing and generates such error
60       * message. This case is special because parser prints error like
61       * {@code "no viable alternative at input 'b \n *\n'"} and it is not
62       * clear that error is about missed close HTML tag.
63       */
64      public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
65  
66      /**
67       * Message key of error message.
68       */
69      public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
70          "javadoc.wrong.singleton.html.tag";
71  
72      /**
73       * Parse error while rule recognition.
74       */
75      public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
76  
77      /**
78       * Message property key for the Unclosed HTML message.
79       */
80      public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
81  
82      /** Symbols with which javadoc starts. */
83      private static final String JAVADOC_START = "/**";
84  
85      /**
86       * Line number of the Block comment AST that is being parsed.
87       */
88      private int blockCommentLineNumber;
89  
90      /**
91       * Parses Javadoc comment as DetailNode tree.
92       *
93       * @param javadocCommentAst
94       *        DetailAST of Javadoc comment
95       * @return DetailNode tree of Javadoc comment
96       */
97      public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
98          blockCommentLineNumber = javadocCommentAst.getLineNo();
99  
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 }