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