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 }