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.utils; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Map; 025import java.util.regex.Pattern; 026 027import javax.annotation.Nullable; 028 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.DetailNode; 031import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 032import com.puppycrawl.tools.checkstyle.api.LineColumn; 033import com.puppycrawl.tools.checkstyle.api.TextBlock; 034import com.puppycrawl.tools.checkstyle.api.TokenTypes; 035import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo; 038import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags; 039import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtil; 040import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtil; 041import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo; 042 043/** 044 * Contains utility methods for working with Javadoc. 045 */ 046public final class JavadocUtil { 047 048 /** 049 * The type of Javadoc tag we want returned. 050 */ 051 public enum JavadocTagType { 052 053 /** Block type. */ 054 BLOCK, 055 /** Inline type. */ 056 INLINE, 057 /** All validTags. */ 058 ALL, 059 060 } 061 062 /** Maps from a token name to value. */ 063 private static final Map<String, Integer> TOKEN_NAME_TO_VALUE; 064 /** Maps from a token value to name. */ 065 private static final Map<Integer, String> TOKEN_VALUE_TO_NAME; 066 067 /** Exception message for unknown JavaDoc token id. */ 068 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc" 069 + " token id. Given id: "; 070 071 /** Newline pattern. */ 072 private static final Pattern NEWLINE = Pattern.compile("\n"); 073 074 /** Return pattern. */ 075 private static final Pattern RETURN = Pattern.compile("\r"); 076 077 /** Tab pattern. */ 078 private static final Pattern TAB = Pattern.compile("\t"); 079 080 // initialise the constants 081 static { 082 TOKEN_NAME_TO_VALUE = 083 TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class); 084 TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE); 085 } 086 087 /** Prevent instantiation. */ 088 private JavadocUtil() { 089 } 090 091 /** 092 * Gets validTags from a given piece of Javadoc. 093 * 094 * @param textBlock 095 * the Javadoc comment to process. 096 * @param tagType 097 * the type of validTags we're interested in 098 * @return all standalone validTags from the given javadoc. 099 */ 100 public static JavadocTags getJavadocTags(TextBlock textBlock, 101 JavadocTagType tagType) { 102 final String[] text = textBlock.getText(); 103 final List<TagInfo> tags = new ArrayList<>(); 104 final boolean isBlockTags = tagType == JavadocTagType.ALL 105 || tagType == JavadocTagType.BLOCK; 106 if (isBlockTags) { 107 tags.addAll(BlockTagUtil.extractBlockTags(text)); 108 } 109 final boolean isInlineTags = tagType == JavadocTagType.ALL 110 || tagType == JavadocTagType.INLINE; 111 if (isInlineTags) { 112 tags.addAll(InlineTagUtil.extractInlineTags(text)); 113 } 114 115 final List<JavadocTag> validTags = new ArrayList<>(); 116 final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); 117 118 for (TagInfo tag : tags) { 119 final LineColumn position = tag.getPosition(); 120 final int col = position.getColumn(); 121 // Add the starting line of the comment to the line number to get the actual line number 122 // in the source. 123 // Lines are one-indexed, so need an off-by-one correction. 124 final int line = textBlock.getStartLineNo() + position.getLine() - 1; 125 126 final String tagName = tag.getName(); 127 if (JavadocTagInfo.isValidName(tagName)) { 128 validTags.add( 129 new JavadocTag(line, col, tagName, tag.getValue())); 130 } 131 else { 132 invalidTags.add(new InvalidJavadocTag(line, col, tagName)); 133 } 134 } 135 136 return new JavadocTags(validTags, invalidTags); 137 } 138 139 /** 140 * Checks that commentContent starts with '*' javadoc comment identifier. 141 * 142 * @param commentContent 143 * content of block comment 144 * @return true if commentContent starts with '*' javadoc comment 145 * identifier. 146 */ 147 public static boolean isJavadocComment(String commentContent) { 148 boolean result = false; 149 150 if (!commentContent.isEmpty()) { 151 final char docCommentIdentifier = commentContent.charAt(0); 152 result = docCommentIdentifier == '*'; 153 } 154 155 return result; 156 } 157 158 /** 159 * Checks block comment content starts with '*' javadoc comment identifier. 160 * 161 * @param blockCommentBegin 162 * block comment AST 163 * @return true if block comment content starts with '*' javadoc comment 164 * identifier. 165 */ 166 public static boolean isJavadocComment(DetailAST blockCommentBegin) { 167 final String commentContent = getBlockCommentContent(blockCommentBegin); 168 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); 169 } 170 171 /** 172 * Gets content of block comment. 173 * 174 * @param blockCommentBegin 175 * block comment AST. 176 * @return content of block comment. 177 */ 178 public static String getBlockCommentContent(DetailAST blockCommentBegin) { 179 final DetailAST commentContent = blockCommentBegin.getFirstChild(); 180 return commentContent.getText(); 181 } 182 183 /** 184 * Get content of Javadoc comment. 185 * 186 * @param javadocCommentBegin 187 * Javadoc comment AST 188 * @return content of Javadoc comment. 189 */ 190 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) { 191 final DetailAST commentContent = javadocCommentBegin.getFirstChild(); 192 return commentContent.getText().substring(1); 193 } 194 195 /** 196 * Returns the Javadoc block comment attached to the given declaration AST node. 197 * 198 * @param ast the declaration AST node 199 * @return the attached Javadoc block comment, or {@code null} if none is found 200 */ 201 @Nullable 202 public static DetailAST getAttachedJavadocComment(final DetailAST ast) { 203 DetailAST result = null; 204 DetailAST child = ast.getFirstChild(); 205 while (result == null && child.getType() != TokenTypes.IDENT) { 206 result = findJavadocComment(child); 207 child = child.getNextSibling(); 208 } 209 return result; 210 } 211 212 /** 213 * Finds the first Javadoc block comment under the given AST node. 214 * 215 * @param ast the AST node to search 216 * @return the Javadoc block comment, or {@code null} if none is found 217 */ 218 @Nullable 219 private static DetailAST findJavadocComment(DetailAST ast) { 220 DetailAST result = null; 221 if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) { 222 result = ast; 223 } 224 else { 225 DetailAST child = ast.getFirstChild(); 226 while (result == null && child != null) { 227 result = findJavadocComment(child); 228 child = child.getNextSibling(); 229 } 230 } 231 return result; 232 } 233 234 /** 235 * Returns the first child token that has a specified type. 236 * 237 * @param detailNode 238 * Javadoc AST node 239 * @param type 240 * the token type to match 241 * @return the matching token, or null if no match 242 */ 243 public static DetailNode findFirstToken(DetailNode detailNode, int type) { 244 DetailNode returnValue = null; 245 DetailNode node = detailNode.getFirstChild(); 246 while (node != null) { 247 if (node.getType() == type) { 248 returnValue = node; 249 break; 250 } 251 node = node.getNextSibling(); 252 } 253 return returnValue; 254 } 255 256 /** 257 * Returns all child tokens that have a specified type. 258 * 259 * @param detailNode Javadoc AST node 260 * @param type the token type to match 261 * @return the matching tokens, or an empty list if no match 262 */ 263 public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) { 264 final List<DetailNode> nodes = new ArrayList<>(); 265 DetailNode node = detailNode.getFirstChild(); 266 while (node != null) { 267 if (node.getType() == type) { 268 nodes.add(node); 269 } 270 node = node.getNextSibling(); 271 } 272 return nodes; 273 } 274 275 /** 276 * Checks whether the given AST node is an HTML element with the specified tag name. 277 * This method ignore void elements. 278 * 279 * @param ast the AST node to check 280 * (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT}) 281 * @param expectedTagName the tag name to match (case-insensitive) 282 * @return {@code true} if the node has the given tag name, {@code false} otherwise 283 */ 284 public static boolean isTag(DetailNode ast, String expectedTagName) { 285 final DetailNode htmlTagStart = findFirstToken(ast, 286 JavadocCommentsTokenTypes.HTML_TAG_START); 287 boolean isTag = false; 288 if (htmlTagStart != null) { 289 final String tagName = findFirstToken(htmlTagStart, 290 JavadocCommentsTokenTypes.TAG_NAME).getText(); 291 isTag = expectedTagName.equalsIgnoreCase(tagName); 292 } 293 return isTag; 294 } 295 296 /** 297 * Gets next sibling of specified node with the specified type. 298 * 299 * @param node DetailNode 300 * @param tokenType javadoc token type 301 * @return next sibling. 302 */ 303 public static DetailNode getNextSibling(DetailNode node, int tokenType) { 304 DetailNode nextSibling = node.getNextSibling(); 305 while (nextSibling != null && nextSibling.getType() != tokenType) { 306 nextSibling = nextSibling.getNextSibling(); 307 } 308 return nextSibling; 309 } 310 311 /** 312 * Returns the name of a token for a given ID. 313 * 314 * @param id 315 * the ID of the token name to get 316 * @return a token name 317 * @throws IllegalArgumentException if an unknown token ID was specified. 318 */ 319 public static String getTokenName(int id) { 320 final String name = TOKEN_VALUE_TO_NAME.get(id); 321 if (name == null) { 322 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 323 } 324 return name; 325 } 326 327 /** 328 * Returns the ID of a token for a given name. 329 * 330 * @param name 331 * the name of the token ID to get 332 * @return a token ID 333 * @throws IllegalArgumentException if an unknown token name was specified. 334 */ 335 public static int getTokenId(String name) { 336 final Integer id = TOKEN_NAME_TO_VALUE.get(name); 337 if (id == null) { 338 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name); 339 } 340 return id; 341 } 342 343 /** 344 * Extracts the tag name from the given Javadoc tag section. 345 * 346 * @param javadocTagSection the node representing a Javadoc tag section. 347 * This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG} 348 * or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}. 349 * @return the tag name (e.g., "param", "return", "link") 350 */ 351 public static String getTagName(DetailNode javadocTagSection) { 352 return findFirstToken(javadocTagSection.getFirstChild(), 353 JavadocCommentsTokenTypes.TAG_NAME).getText(); 354 } 355 356 /** 357 * Replace all control chars with escaped symbols. 358 * 359 * @param text the String to process. 360 * @return the processed String with all control chars escaped. 361 */ 362 public static String escapeAllControlChars(String text) { 363 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); 364 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); 365 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); 366 } 367 368 /** 369 * Checks Javadoc comment it's in right place. 370 * 371 * <p>From Javadoc util documentation: 372 * "Placement of comments - Documentation comments are recognized only when placed 373 * immediately before class, interface, constructor, method, field or annotation field 374 * declarations -- see the class example, method example, and field example. 375 * Documentation comments placed in the body of a method are ignored."</p> 376 * 377 * <p>If there are many documentation comments per declaration statement, 378 * only the last one will be recognized.</p> 379 * 380 * @param blockComment Block comment AST 381 * @return true if Javadoc is in right place 382 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html"> 383 * Javadoc util documentation</a> 384 */ 385 public static boolean isCorrectJavadocPosition(DetailAST blockComment) { 386 // We must be sure that after this one there are no other documentation comments. 387 DetailAST sibling = blockComment.getNextSibling(); 388 while (sibling != null) { 389 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 390 if (isJavadocComment(getBlockCommentContent(sibling))) { 391 // Found another javadoc comment, so this one should be ignored. 392 break; 393 } 394 sibling = sibling.getNextSibling(); 395 } 396 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 397 sibling = sibling.getNextSibling(); 398 } 399 else { 400 // Annotation, declaration or modifier is here. Do not check further. 401 sibling = null; 402 } 403 } 404 return sibling == null 405 && (BlockCommentPosition.isOnType(blockComment) 406 || BlockCommentPosition.isOnMember(blockComment) 407 || BlockCommentPosition.isOnPackage(blockComment)); 408 } 409 410}