001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.lang.reflect.Field; 023import java.lang.reflect.Modifier; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.google.common.collect.ImmutableMap; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.DetailNode; 032import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 033import com.puppycrawl.tools.checkstyle.api.TextBlock; 034import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag; 035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo; 037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags; 038 039/** 040 * Contains utility methods for working with Javadoc. 041 * @author Lyle Hanson 042 */ 043public final class JavadocUtils { 044 045 /** 046 * The type of Javadoc tag we want returned. 047 */ 048 public enum JavadocTagType { 049 /** Block type. */ 050 BLOCK, 051 /** Inline type. */ 052 INLINE, 053 /** All validTags. */ 054 ALL 055 } 056 057 /** Maps from a token name to value. */ 058 private static final ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE; 059 /** Maps from a token value to name. */ 060 private static final String[] TOKEN_VALUE_TO_NAME; 061 062 /** Exception message for unknown JavaDoc token id. */ 063 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc" 064 + " token id. Given id: "; 065 066 /** Comment pattern. */ 067 private static final Pattern COMMENT_PATTERN = Pattern.compile( 068 "^\\s*(?:/\\*{2,}|\\*+)\\s*(.*)"); 069 070 /** Block tag pattern for a first line. */ 071 private static final Pattern BLOCK_TAG_PATTERN_FIRST_LINE = Pattern.compile( 072 "/\\*{2,}\\s*@(\\p{Alpha}+)\\s"); 073 074 /** Block tag pattern. */ 075 private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile( 076 "^\\s*\\**\\s*@(\\p{Alpha}+)\\s"); 077 078 /** Inline tag pattern. */ 079 private static final Pattern INLINE_TAG_PATTERN = Pattern.compile( 080 ".*?\\{@(\\p{Alpha}+)\\s+(.*?)}"); 081 082 /** Newline pattern. */ 083 private static final Pattern NEWLINE = Pattern.compile("\n"); 084 085 /** Return pattern. */ 086 private static final Pattern RETURN = Pattern.compile("\r"); 087 088 /** Tab pattern. */ 089 private static final Pattern TAB = Pattern.compile("\t"); 090 091 // Using reflection gets all token names and values from JavadocTokenTypes class 092 // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections. 093 static { 094 final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder(); 095 096 final Field[] fields = JavadocTokenTypes.class.getDeclaredFields(); 097 098 String[] tempTokenValueToName = CommonUtils.EMPTY_STRING_ARRAY; 099 100 for (final Field field : fields) { 101 102 // Only process public int fields. 103 if (!Modifier.isPublic(field.getModifiers()) 104 || field.getType() != Integer.TYPE) { 105 continue; 106 } 107 108 final String name = field.getName(); 109 110 final int tokenValue = TokenUtils.getIntFromField(field, name); 111 builder.put(name, tokenValue); 112 if (tokenValue > tempTokenValueToName.length - 1) { 113 final String[] temp = new String[tokenValue + 1]; 114 System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length); 115 tempTokenValueToName = temp; 116 } 117 if (tokenValue == -1) { 118 tempTokenValueToName[0] = name; 119 } 120 else { 121 tempTokenValueToName[tokenValue] = name; 122 } 123 } 124 125 TOKEN_NAME_TO_VALUE = builder.build(); 126 TOKEN_VALUE_TO_NAME = tempTokenValueToName; 127 } 128 129 /** Prevent instantiation. */ 130 private JavadocUtils() { 131 } 132 133 /** 134 * Gets validTags from a given piece of Javadoc. 135 * @param textBlock 136 * the Javadoc comment to process. 137 * @param tagType 138 * the type of validTags we're interested in 139 * @return all standalone validTags from the given javadoc. 140 */ 141 public static JavadocTags getJavadocTags(TextBlock textBlock, 142 JavadocTagType tagType) { 143 final String[] text = textBlock.getText(); 144 final List<JavadocTag> tags = new ArrayList<>(); 145 final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); 146 for (int i = 0; i < text.length; i++) { 147 final String textValue = text[i]; 148 final Matcher blockTagMatcher = getBlockTagPattern(i).matcher(textValue); 149 if ((tagType == JavadocTagType.ALL || tagType == JavadocTagType.BLOCK) 150 && blockTagMatcher.find()) { 151 final String tagName = blockTagMatcher.group(1); 152 String content = textValue.substring(blockTagMatcher.end(1)); 153 if (content.endsWith("*/")) { 154 content = content.substring(0, content.length() - 2); 155 } 156 final int line = textBlock.getStartLineNo() + i; 157 int col = blockTagMatcher.start(1) - 1; 158 if (i == 0) { 159 col += textBlock.getStartColNo(); 160 } 161 if (JavadocTagInfo.isValidName(tagName)) { 162 tags.add( 163 new JavadocTag(line, col, tagName, content.trim())); 164 } 165 else { 166 invalidTags.add(new InvalidJavadocTag(line, col, tagName)); 167 } 168 } 169 // No block tag, so look for inline validTags 170 else if (tagType == JavadocTagType.ALL || tagType == JavadocTagType.INLINE) { 171 lookForInlineTags(textBlock, i, tags, invalidTags); 172 } 173 } 174 return new JavadocTags(tags, invalidTags); 175 } 176 177 /** 178 * Get a block tag pattern depending on a line number of a javadoc. 179 * @param lineNumber the line number. 180 * @return a block tag pattern. 181 */ 182 private static Pattern getBlockTagPattern(int lineNumber) { 183 final Pattern blockTagPattern; 184 if (lineNumber == 0) { 185 blockTagPattern = BLOCK_TAG_PATTERN_FIRST_LINE; 186 } 187 else { 188 blockTagPattern = BLOCK_TAG_PATTERN; 189 } 190 return blockTagPattern; 191 } 192 193 /** 194 * Looks for inline tags in comment and adds them to the proper tags collection. 195 * @param comment comment text block 196 * @param lineNumber line number in the comment 197 * @param validTags collection of valid tags 198 * @param invalidTags collection of invalid tags 199 */ 200 private static void lookForInlineTags(TextBlock comment, int lineNumber, 201 final List<JavadocTag> validTags, final List<InvalidJavadocTag> invalidTags) { 202 final String text = comment.getText()[lineNumber]; 203 // Match Javadoc text after comment characters 204 final Matcher commentMatcher = COMMENT_PATTERN.matcher(text); 205 final String commentContents; 206 207 // offset including comment characters 208 final int commentOffset; 209 210 if (commentMatcher.find()) { 211 commentContents = commentMatcher.group(1); 212 commentOffset = commentMatcher.start(1) - 1; 213 } 214 else { 215 // No leading asterisks, still valid 216 commentContents = text; 217 commentOffset = 0; 218 } 219 final Matcher tagMatcher = INLINE_TAG_PATTERN.matcher(commentContents); 220 while (tagMatcher.find()) { 221 final String tagName = tagMatcher.group(1); 222 final int line = comment.getStartLineNo() + lineNumber; 223 int col = commentOffset + tagMatcher.start(1) - 1; 224 if (lineNumber == 0) { 225 col += comment.getStartColNo(); 226 } 227 if (JavadocTagInfo.isValidName(tagName)) { 228 final String tagValue = tagMatcher.group(2).trim(); 229 validTags.add(new JavadocTag(line, col, tagName, 230 tagValue)); 231 } 232 else { 233 invalidTags.add(new InvalidJavadocTag(line, col, 234 tagName)); 235 } 236 } 237 } 238 239 /** 240 * Checks that commentContent starts with '*' javadoc comment identifier. 241 * @param commentContent 242 * content of block comment 243 * @return true if commentContent starts with '*' javadoc comment 244 * identifier. 245 */ 246 public static boolean isJavadocComment(String commentContent) { 247 boolean result = false; 248 249 if (!commentContent.isEmpty()) { 250 final char docCommentIdentificator = commentContent.charAt(0); 251 result = docCommentIdentificator == '*'; 252 } 253 254 return result; 255 } 256 257 /** 258 * Checks block comment content starts with '*' javadoc comment identifier. 259 * @param blockCommentBegin 260 * block comment AST 261 * @return true if block comment content starts with '*' javadoc comment 262 * identifier. 263 */ 264 public static boolean isJavadocComment(DetailAST blockCommentBegin) { 265 final String commentContent = getBlockCommentContent(blockCommentBegin); 266 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); 267 } 268 269 /** 270 * Gets content of block comment. 271 * @param blockCommentBegin 272 * block comment AST. 273 * @return content of block comment. 274 */ 275 private static String getBlockCommentContent(DetailAST blockCommentBegin) { 276 final DetailAST commentContent = blockCommentBegin.getFirstChild(); 277 return commentContent.getText(); 278 } 279 280 /** 281 * Get content of Javadoc comment. 282 * @param javadocCommentBegin 283 * Javadoc comment AST 284 * @return content of Javadoc comment. 285 */ 286 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) { 287 final DetailAST commentContent = javadocCommentBegin.getFirstChild(); 288 return commentContent.getText().substring(1); 289 } 290 291 /** 292 * Returns the first child token that has a specified type. 293 * @param detailNode 294 * Javadoc AST node 295 * @param type 296 * the token type to match 297 * @return the matching token, or null if no match 298 */ 299 public static DetailNode findFirstToken(DetailNode detailNode, int type) { 300 DetailNode returnValue = null; 301 DetailNode node = getFirstChild(detailNode); 302 while (node != null) { 303 if (node.getType() == type) { 304 returnValue = node; 305 break; 306 } 307 node = getNextSibling(node); 308 } 309 return returnValue; 310 } 311 312 /** 313 * Gets first child node of specified node. 314 * 315 * @param node DetailNode 316 * @return first child 317 */ 318 public static DetailNode getFirstChild(DetailNode node) { 319 DetailNode resultNode = null; 320 321 if (node.getChildren().length > 0) { 322 resultNode = node.getChildren()[0]; 323 } 324 return resultNode; 325 } 326 327 /** 328 * Checks whether node contains any node of specified type among children on any deep level. 329 * 330 * @param node DetailNode 331 * @param type token type 332 * @return true if node contains any node of type type among children on any deep level. 333 */ 334 public static boolean containsInBranch(DetailNode node, int type) { 335 boolean result = true; 336 DetailNode curNode = node; 337 while (type != curNode.getType()) { 338 DetailNode toVisit = getFirstChild(curNode); 339 while (curNode != null && toVisit == null) { 340 toVisit = getNextSibling(curNode); 341 if (toVisit == null) { 342 curNode = curNode.getParent(); 343 } 344 } 345 346 if (curNode == toVisit) { 347 result = false; 348 break; 349 } 350 351 curNode = toVisit; 352 } 353 return result; 354 } 355 356 /** 357 * Gets next sibling of specified node. 358 * 359 * @param node DetailNode 360 * @return next sibling. 361 */ 362 public static DetailNode getNextSibling(DetailNode node) { 363 DetailNode nextSibling = null; 364 final DetailNode parent = node.getParent(); 365 if (parent != null) { 366 final int nextSiblingIndex = node.getIndex() + 1; 367 final DetailNode[] children = parent.getChildren(); 368 if (nextSiblingIndex <= children.length - 1) { 369 nextSibling = children[nextSiblingIndex]; 370 } 371 } 372 return nextSibling; 373 } 374 375 /** 376 * Gets next sibling of specified node with the specified type. 377 * 378 * @param node DetailNode 379 * @param tokenType javadoc token type 380 * @return next sibling. 381 */ 382 public static DetailNode getNextSibling(DetailNode node, int tokenType) { 383 DetailNode nextSibling = getNextSibling(node); 384 while (nextSibling != null && nextSibling.getType() != tokenType) { 385 nextSibling = getNextSibling(nextSibling); 386 } 387 return nextSibling; 388 } 389 390 /** 391 * Gets previous sibling of specified node. 392 * @param node DetailNode 393 * @return previous sibling 394 */ 395 public static DetailNode getPreviousSibling(DetailNode node) { 396 DetailNode previousSibling = null; 397 final int previousSiblingIndex = node.getIndex() - 1; 398 if (previousSiblingIndex >= 0) { 399 final DetailNode parent = node.getParent(); 400 final DetailNode[] children = parent.getChildren(); 401 previousSibling = children[previousSiblingIndex]; 402 } 403 return previousSibling; 404 } 405 406 /** 407 * Returns the name of a token for a given ID. 408 * @param id 409 * the ID of the token name to get 410 * @return a token name 411 */ 412 public static String getTokenName(int id) { 413 final String name; 414 if (id == JavadocTokenTypes.EOF) { 415 name = "EOF"; 416 } 417 else if (id > TOKEN_VALUE_TO_NAME.length - 1) { 418 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 419 } 420 else { 421 name = TOKEN_VALUE_TO_NAME[id]; 422 if (name == null) { 423 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 424 } 425 } 426 return name; 427 } 428 429 /** 430 * Returns the ID of a token for a given name. 431 * @param name 432 * the name of the token ID to get 433 * @return a token ID 434 */ 435 public static int getTokenId(String name) { 436 final Integer id = TOKEN_NAME_TO_VALUE.get(name); 437 if (id == null) { 438 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name); 439 } 440 return id; 441 } 442 443 /** 444 * Gets tag name from javadocTagSection. 445 * 446 * @param javadocTagSection to get tag name from. 447 * @return name, of the javadocTagSection's tag. 448 */ 449 public static String getTagName(DetailNode javadocTagSection) { 450 final String javadocTagName; 451 if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 452 javadocTagName = getNextSibling( 453 getFirstChild(javadocTagSection)).getText(); 454 } 455 else { 456 javadocTagName = getFirstChild(javadocTagSection).getText(); 457 } 458 return javadocTagName; 459 } 460 461 /** 462 * Replace all control chars with escaped symbols. 463 * @param text the String to process. 464 * @return the processed String with all control chars escaped. 465 */ 466 public static String escapeAllControlChars(String text) { 467 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); 468 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); 469 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); 470 } 471 472 /** 473 * Checks Javadoc comment it's in right place. 474 * From Javadoc util documentation: 475 * "Placement of comments - Documentation comments are recognized only when placed 476 * immediately before class, interface, constructor, method, or field 477 * declarations -- see the class example, method example, and field example. 478 * Documentation comments placed in the body of a method are ignored. Only one 479 * documentation comment per declaration statement is recognized by the Javadoc tool." 480 * 481 * @param blockComment Block comment AST 482 * @return true if Javadoc is in right place 483 */ 484 public static boolean isCorrectJavadocPosition(DetailAST blockComment) { 485 return BlockCommentPosition.isOnClass(blockComment) 486 || BlockCommentPosition.isOnInterface(blockComment) 487 || BlockCommentPosition.isOnEnum(blockComment) 488 || BlockCommentPosition.isOnMethod(blockComment) 489 || BlockCommentPosition.isOnField(blockComment) 490 || BlockCommentPosition.isOnConstructor(blockComment) 491 || BlockCommentPosition.isOnEnumConstant(blockComment) 492 || BlockCommentPosition.isOnAnnotationDef(blockComment); 493 } 494}