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