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