View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.Set;
23  
24  import org.antlr.v4.runtime.BaseErrorListener;
25  import org.antlr.v4.runtime.CharStreams;
26  import org.antlr.v4.runtime.CommonTokenStream;
27  import org.antlr.v4.runtime.RecognitionException;
28  import org.antlr.v4.runtime.Recognizer;
29  import org.antlr.v4.runtime.atn.PredictionMode;
30  import org.antlr.v4.runtime.misc.ParseCancellationException;
31  
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.DetailNode;
34  import com.puppycrawl.tools.checkstyle.grammar.SimpleToken;
35  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsLexer;
36  import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParser;
37  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
38  
39  /**
40   * Used for parsing Javadoc comment as DetailNode tree.
41   *
42   */
43  public class JavadocDetailNodeParser {
44  
45      /**
46       * Parse error while rule recognition.
47       */
48      public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
49  
50      /**
51       * Message property key for the Unclosed HTML message.
52       */
53      public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
54  
55      /** Symbols with which javadoc starts. */
56      private static final String JAVADOC_START = "/**";
57  
58      /**
59       * Creates a new {@code JavadocDetailNodeParser} instance.
60       */
61      public JavadocDetailNodeParser() {
62          // no code by default
63      }
64  
65      /**
66       * Parses the given Javadoc comment AST into a {@link ParseStatus} object.
67       *
68       * <p>
69       * This method extracts the raw Javadoc comment text from the supplied
70       * {@link DetailAST}, creates a new lexer and parser for the Javadoc grammar,
71       * and attempts to parse it into an AST of {@link DetailNode}s.
72       * The parser uses {@link PredictionMode#SLL} for
73       * faster performance and stops parsing on the first error encountered by
74       * using {@link CheckstyleParserErrorStrategy}.
75       * </p>
76       *
77       * @param javadocCommentAst
78       *        the {@link DetailAST} node representing the Javadoc comment in the
79       *        source file
80       * @return a {@link ParseStatus} containing the root of the parsed Javadoc
81       *        tree (if successful), the first non-tight HTML tag (if any), and
82       *        the error message (if parsing failed)
83       */
84      public ParseStatus parseJavadocComment(DetailAST javadocCommentAst) {
85          final int blockCommentLineNumber = javadocCommentAst.getLineNo();
86  
87          final String javadocComment = JavadocUtil.getJavadocCommentContent(javadocCommentAst);
88          final ParseStatus result = new ParseStatus();
89  
90          // Use a new error listener each time to be able to use
91          // one check instance for multiple files to be checked
92          // without getting side effects.
93          final DescriptiveErrorListener errorListener = new DescriptiveErrorListener();
94  
95          // Log messages should have line number in scope of file,
96          // not in scope of Javadoc comment.
97          // Offset is line number of beginning of Javadoc comment.
98          errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
99  
100         final JavadocCommentsLexer lexer =
101                         new JavadocCommentsLexer(CharStreams.fromString(javadocComment), true);
102 
103         lexer.removeErrorListeners();
104         lexer.addErrorListener(errorListener);
105 
106         final CommonTokenStream tokens = new CommonTokenStream(lexer);
107         tokens.fill();
108 
109         final Set<SimpleToken> unclosedTags = lexer.getUnclosedTagNameTokens();
110         final JavadocCommentsParser parser = new JavadocCommentsParser(tokens, unclosedTags);
111 
112         // set prediction mode to SLL to speed up parsing
113         parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
114 
115         // remove default error listeners
116         parser.removeErrorListeners();
117 
118         parser.addErrorListener(errorListener);
119 
120         // JavadocParserErrorStrategy stops parsing on first parse error encountered unlike the
121         // DefaultErrorStrategy used by ANTLR which rather attempts error recovery.
122         parser.setErrorHandler(new CheckstyleParserErrorStrategy());
123 
124         try {
125             final JavadocCommentsParser.JavadocContext javadoc = parser.javadoc();
126             final int javadocColumnNumber = javadocCommentAst.getColumnNo()
127                             + JAVADOC_START.length();
128 
129             final JavadocCommentsAstVisitor visitor = new JavadocCommentsAstVisitor(
130                     tokens, blockCommentLineNumber, javadocColumnNumber);
131             final DetailNode tree = visitor.visit(javadoc);
132 
133             result.setTree(tree);
134 
135             result.firstNonTightHtmlTag = visitor.getFirstNonTightHtmlTag();
136 
137             result.setParseErrorMessage(errorListener.getErrorMessage());
138         }
139         catch (ParseCancellationException | IllegalArgumentException exc) {
140             result.setParseErrorMessage(errorListener.getErrorMessage());
141         }
142 
143         return result;
144     }
145 
146     /**
147      * Custom error listener for JavadocParser that prints user readable errors.
148      */
149     private static final class DescriptiveErrorListener extends BaseErrorListener {
150 
151         /**
152          * Offset is line number of beginning of the Javadoc comment. Log
153          * messages should have line number in scope of file, not in scope of
154          * Javadoc comment.
155          */
156         private int offset;
157 
158         /**
159          * Error message that appeared while parsing.
160          */
161         private ParseErrorMessage errorMessage;
162 
163         /**
164          * Creates a new {@code DescriptiveErrorListener} instance.
165          */
166         private DescriptiveErrorListener() {
167             // no code by default
168         }
169 
170         /**
171          * Getter for error message during parsing.
172          *
173          * @return Error message during parsing.
174          */
175         private ParseErrorMessage getErrorMessage() {
176             return errorMessage;
177         }
178 
179         /**
180          * Sets offset. Offset is line number of beginning of the Javadoc
181          * comment. Log messages should have line number in scope of file, not
182          * in scope of Javadoc comment.
183          *
184          * @param offset
185          *        offset line number
186          */
187         /* package */ void setOffset(int offset) {
188             this.offset = offset;
189         }
190 
191         /**
192          * Logs parser errors in Checkstyle manner. Parser can generate error
193          * messages. There is special error that parser can generate. It is
194          * missed close HTML tag. This case is special because parser prints
195          * error like {@code "no viable alternative at input 'b \n *\n'"} and it
196          * is not clear that error is about missed close HTML tag. Other error
197          * messages are not special and logged simply as "Parse Error...".
198          *
199          * <p>{@inheritDoc}
200          */
201         @Override
202         public void syntaxError(
203                 Recognizer<?, ?> recognizer, Object offendingSymbol,
204                 int line, int charPositionInLine,
205                 String msg, RecognitionException ex) {
206             final int lineNumber = offset + line;
207 
208             final String target;
209             if (recognizer instanceof JavadocCommentsLexer lexer) {
210                 target = lexer.getPreviousToken().getText();
211             }
212             else {
213                 final int ruleIndex = ex.getCtx().getRuleIndex();
214                 final String ruleName = recognizer.getRuleNames()[ruleIndex];
215                 target = convertUpperCamelToUpperUnderscore(ruleName);
216             }
217 
218             errorMessage = new ParseErrorMessage(lineNumber,
219                     MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, target);
220 
221         }
222 
223         /**
224          * Converts the given {@code text} from camel case to all upper case with
225          * underscores separating each word.
226          *
227          * @param text The string to convert.
228          * @return The result of the conversion.
229          */
230         private static String convertUpperCamelToUpperUnderscore(String text) {
231             final StringBuilder result = new StringBuilder(20);
232             for (int index = 0; index < text.length(); index++) {
233                 final char letter = text.charAt(index);
234                 if (Character.isUpperCase(letter)) {
235                     result.append('_');
236                 }
237                 result.append(Character.toUpperCase(letter));
238             }
239             return result.toString();
240         }
241     }
242 
243     /**
244      * Contains result of parsing javadoc comment: DetailNode tree and parse
245      * error message.
246      */
247     public static class ParseStatus {
248 
249         /**
250          * DetailNode tree (is null if parsing fails).
251          */
252         private DetailNode tree;
253 
254         /**
255          * Parse error message (is null if parsing is successful).
256          */
257         private ParseErrorMessage parseErrorMessage;
258 
259         /**
260          * Stores the first non-tight HTML tag encountered while parsing javadoc.
261          *
262          * @see <a
263          *     href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
264          *     Tight HTML rules</a>
265          */
266         private DetailNode firstNonTightHtmlTag;
267 
268         /**
269          * Creates a new {@code ParseStatus} instance.
270          */
271         public ParseStatus() {
272             // no code by default
273         }
274 
275         /**
276          * Getter for DetailNode tree.
277          *
278          * @return DetailNode tree if parsing was successful, null otherwise.
279          */
280         public DetailNode getTree() {
281             return tree;
282         }
283 
284         /**
285          * Sets DetailNode tree.
286          *
287          * @param tree DetailNode tree.
288          */
289         public void setTree(DetailNode tree) {
290             this.tree = tree;
291         }
292 
293         /**
294          * Getter for error message during parsing.
295          *
296          * @return Error message if parsing was unsuccessful, null otherwise.
297          */
298         public ParseErrorMessage getParseErrorMessage() {
299             return parseErrorMessage;
300         }
301 
302         /**
303          * Sets parse error message.
304          *
305          * @param parseErrorMessage Parse error message.
306          */
307         public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
308             this.parseErrorMessage = parseErrorMessage;
309         }
310 
311         /**
312          * This method is used to check if the javadoc parsed has non-tight HTML tags.
313          *
314          * @return returns true if the javadoc has at least one non-tight HTML tag; false otherwise
315          * @see <a
316          *     href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
317          *     Tight HTML rules</a>
318          */
319         public boolean isNonTight() {
320             return firstNonTightHtmlTag != null;
321         }
322 
323         /**
324          * Getter for the first non-tight HTML tag encountered while parsing javadoc.
325          *
326          * @return the first non-tight HTML tag that is encountered while parsing Javadoc,
327          *     if one exists
328          * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
329          *     Tight HTML rules</a>
330          */
331         public DetailNode getFirstNonTightHtmlTag() {
332             return firstNonTightHtmlTag;
333         }
334 
335     }
336 
337     /**
338      * Contains information about parse error message.
339      */
340     public static class ParseErrorMessage {
341 
342         /**
343          * Line number where parse error occurred.
344          */
345         private final int lineNumber;
346 
347         /**
348          * Key for error message.
349          */
350         private final String messageKey;
351 
352         /**
353          * Error message arguments.
354          */
355         private final Object[] messageArguments;
356 
357         /**
358          * Initializes parse error message.
359          *
360          * @param lineNumber line number
361          * @param messageKey message key
362          * @param messageArguments message arguments
363          */
364         /* package */ ParseErrorMessage(int lineNumber, String messageKey,
365                 Object... messageArguments) {
366             this.lineNumber = lineNumber;
367             this.messageKey = messageKey;
368             this.messageArguments = messageArguments.clone();
369         }
370 
371         /**
372          * Getter for line number where parse error occurred.
373          *
374          * @return Line number where parse error occurred.
375          */
376         public int getLineNumber() {
377             return lineNumber;
378         }
379 
380         /**
381          * Getter for key for error message.
382          *
383          * @return Key for error message.
384          */
385         public String getMessageKey() {
386             return messageKey;
387         }
388 
389         /**
390          * Getter for error message arguments.
391          *
392          * @return Array of error message arguments.
393          */
394         public Object[] getMessageArguments() {
395             return messageArguments.clone();
396         }
397 
398     }
399 
400 }