001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle; 021 022import java.util.Set; 023 024import org.antlr.v4.runtime.BaseErrorListener; 025import org.antlr.v4.runtime.CharStreams; 026import org.antlr.v4.runtime.CommonTokenStream; 027import org.antlr.v4.runtime.RecognitionException; 028import org.antlr.v4.runtime.Recognizer; 029import org.antlr.v4.runtime.atn.PredictionMode; 030import org.antlr.v4.runtime.misc.ParseCancellationException; 031 032import com.puppycrawl.tools.checkstyle.api.DetailAST; 033import com.puppycrawl.tools.checkstyle.api.DetailNode; 034import com.puppycrawl.tools.checkstyle.grammar.SimpleToken; 035import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsLexer; 036import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParser; 037import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 038 039/** 040 * Used for parsing Javadoc comment as DetailNode tree. 041 * 042 */ 043public class JavadocDetailNodeParser { 044 045 /** 046 * Parse error while rule recognition. 047 */ 048 public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error"; 049 050 /** 051 * Message property key for the Unclosed HTML message. 052 */ 053 public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml"; 054 055 /** Symbols with which javadoc starts. */ 056 private static final String JAVADOC_START = "/**"; 057 058 /** 059 * Creates a new {@code JavadocDetailNodeParser} instance. 060 */ 061 public JavadocDetailNodeParser() { 062 // no code by default 063 } 064 065 /** 066 * Parses the given Javadoc comment AST into a {@link ParseStatus} object. 067 * 068 * <p> 069 * This method extracts the raw Javadoc comment text from the supplied 070 * {@link DetailAST}, creates a new lexer and parser for the Javadoc grammar, 071 * and attempts to parse it into an AST of {@link DetailNode}s. 072 * The parser uses {@link PredictionMode#SLL} for 073 * faster performance and stops parsing on the first error encountered by 074 * using {@link CheckstyleParserErrorStrategy}. 075 * </p> 076 * 077 * @param javadocCommentAst 078 * the {@link DetailAST} node representing the Javadoc comment in the 079 * source file 080 * @return a {@link ParseStatus} containing the root of the parsed Javadoc 081 * tree (if successful), the first non-tight HTML tag (if any), and 082 * the error message (if parsing failed) 083 */ 084 public ParseStatus parseJavadocComment(DetailAST javadocCommentAst) { 085 final int blockCommentLineNumber = javadocCommentAst.getLineNo(); 086 087 final String javadocComment = JavadocUtil.getJavadocCommentContent(javadocCommentAst); 088 final ParseStatus result = new ParseStatus(); 089 090 // Use a new error listener each time to be able to use 091 // one check instance for multiple files to be checked 092 // without getting side effects. 093 final DescriptiveErrorListener errorListener = new DescriptiveErrorListener(); 094 095 // Log messages should have line number in scope of file, 096 // not in scope of Javadoc comment. 097 // Offset is line number of beginning of Javadoc comment. 098 errorListener.setOffset(javadocCommentAst.getLineNo() - 1); 099 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}