001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.Arrays; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Pattern; 028import java.util.stream.Collectors; 029 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.FullIdent; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 034 035/** 036 * Contains utility methods for the checks. 037 * 038 */ 039public final class CheckUtil { 040 041 // constants for parseDouble() 042 /** Binary radix. */ 043 private static final int BASE_2 = 2; 044 045 /** Octal radix. */ 046 private static final int BASE_8 = 8; 047 048 /** Decimal radix. */ 049 private static final int BASE_10 = 10; 050 051 /** Hex radix. */ 052 private static final int BASE_16 = 16; 053 054 /** Maximum children allowed in setter/getter. */ 055 private static final int SETTER_GETTER_MAX_CHILDREN = 7; 056 057 /** Maximum nodes allowed in a body of setter. */ 058 private static final int SETTER_BODY_SIZE = 3; 059 060 /** Maximum nodes allowed in a body of getter. */ 061 private static final int GETTER_BODY_SIZE = 2; 062 063 /** Pattern matching underscore characters ('_'). */ 064 private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_"); 065 066 /** Pattern matching names of setter methods. */ 067 private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*"); 068 069 /** Pattern matching names of getter methods. */ 070 private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*"); 071 072 /** Compiled pattern for all system newlines. */ 073 private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R"); 074 075 /** Package separator. */ 076 private static final char PACKAGE_SEPARATOR = '.'; 077 078 /** Prevent instances. */ 079 private CheckUtil() { 080 } 081 082 /** 083 * Tests whether a method definition AST defines an equals covariant. 084 * 085 * @param ast the method definition AST to test. 086 * Precondition: ast is a TokenTypes.METHOD_DEF node. 087 * @return true if ast defines an equals covariant. 088 */ 089 public static boolean isEqualsMethod(DetailAST ast) { 090 boolean equalsMethod = false; 091 092 if (ast.getType() == TokenTypes.METHOD_DEF) { 093 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 094 final boolean staticOrAbstract = 095 modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null 096 || modifiers.findFirstToken(TokenTypes.ABSTRACT) != null; 097 098 if (!staticOrAbstract) { 099 final DetailAST nameNode = ast.findFirstToken(TokenTypes.IDENT); 100 final String name = nameNode.getText(); 101 102 if ("equals".equals(name)) { 103 // one parameter? 104 final DetailAST paramsNode = ast.findFirstToken(TokenTypes.PARAMETERS); 105 equalsMethod = paramsNode.getChildCount() == 1; 106 } 107 } 108 } 109 return equalsMethod; 110 } 111 112 /** 113 * Returns whether a token represents an ELSE as part of an ELSE / IF set. 114 * 115 * @param ast the token to check 116 * @return whether it is 117 */ 118 public static boolean isElseIf(DetailAST ast) { 119 final DetailAST parentAST = ast.getParent(); 120 121 return ast.getType() == TokenTypes.LITERAL_IF 122 && (isElse(parentAST) || isElseWithCurlyBraces(parentAST)); 123 } 124 125 /** 126 * Returns whether a token represents an ELSE. 127 * 128 * @param ast the token to check 129 * @return whether the token represents an ELSE 130 */ 131 private static boolean isElse(DetailAST ast) { 132 return ast.getType() == TokenTypes.LITERAL_ELSE; 133 } 134 135 /** 136 * Returns whether a token represents an SLIST as part of an ELSE 137 * statement. 138 * 139 * @param ast the token to check 140 * @return whether the toke does represent an SLIST as part of an ELSE 141 */ 142 private static boolean isElseWithCurlyBraces(DetailAST ast) { 143 return ast.getType() == TokenTypes.SLIST 144 && ast.getChildCount() == 2 145 && isElse(ast.getParent()); 146 } 147 148 /** 149 * Returns the value represented by the specified string of the specified 150 * type. Returns 0 for types other than float, double, int, and long. 151 * 152 * @param text the string to be parsed. 153 * @param type the token type of the text. Should be a constant of 154 * {@link TokenTypes}. 155 * @return the double value represented by the string argument. 156 */ 157 public static double parseDouble(String text, int type) { 158 String txt = UNDERSCORE_PATTERN.matcher(text).replaceAll(""); 159 final double result; 160 switch (type) { 161 case TokenTypes.NUM_FLOAT: 162 case TokenTypes.NUM_DOUBLE: 163 result = Double.parseDouble(txt); 164 break; 165 case TokenTypes.NUM_INT: 166 case TokenTypes.NUM_LONG: 167 int radix = BASE_10; 168 if (txt.startsWith("0x") || txt.startsWith("0X")) { 169 radix = BASE_16; 170 txt = txt.substring(2); 171 } 172 else if (txt.startsWith("0b") || txt.startsWith("0B")) { 173 radix = BASE_2; 174 txt = txt.substring(2); 175 } 176 else if (CommonUtil.startsWithChar(txt, '0')) { 177 radix = BASE_8; 178 txt = txt.substring(1); 179 } 180 result = parseNumber(txt, radix, type); 181 break; 182 default: 183 result = Double.NaN; 184 break; 185 } 186 return result; 187 } 188 189 /** 190 * Parses the string argument as an integer or a long in the radix specified by 191 * the second argument. The characters in the string must all be digits of 192 * the specified radix. 193 * 194 * @param text the String containing the integer representation to be 195 * parsed. Precondition: text contains a parsable int. 196 * @param radix the radix to be used while parsing text. 197 * @param type the token type of the text. Should be a constant of 198 * {@link TokenTypes}. 199 * @return the number represented by the string argument in the specified radix. 200 */ 201 private static double parseNumber(final String text, final int radix, final int type) { 202 String txt = text; 203 if (CommonUtil.endsWithChar(txt, 'L') || CommonUtil.endsWithChar(txt, 'l')) { 204 txt = txt.substring(0, txt.length() - 1); 205 } 206 final double result; 207 if (txt.isEmpty()) { 208 result = 0.0; 209 } 210 else { 211 final boolean negative = txt.charAt(0) == '-'; 212 if (type == TokenTypes.NUM_INT) { 213 if (negative) { 214 result = Integer.parseInt(txt, radix); 215 } 216 else { 217 result = Integer.parseUnsignedInt(txt, radix); 218 } 219 } 220 else { 221 if (negative) { 222 result = Long.parseLong(txt, radix); 223 } 224 else { 225 result = Long.parseUnsignedLong(txt, radix); 226 } 227 } 228 } 229 return result; 230 } 231 232 /** 233 * Finds sub-node for given node minimal (line, column) pair. 234 * 235 * @param node the root of tree for search. 236 * @return sub-node with minimal (line, column) pair. 237 */ 238 public static DetailAST getFirstNode(final DetailAST node) { 239 DetailAST currentNode = node; 240 DetailAST child = node.getFirstChild(); 241 while (child != null) { 242 final DetailAST newNode = getFirstNode(child); 243 if (isBeforeInSource(newNode, currentNode)) { 244 currentNode = newNode; 245 } 246 child = child.getNextSibling(); 247 } 248 249 return currentNode; 250 } 251 252 /** 253 * Retrieves whether ast1 is located before ast2. 254 * 255 * @param ast1 the first node. 256 * @param ast2 the second node. 257 * @return true, if ast1 is located before ast2. 258 */ 259 public static boolean isBeforeInSource(DetailAST ast1, DetailAST ast2) { 260 return ast1.getLineNo() < ast2.getLineNo() 261 || TokenUtil.areOnSameLine(ast1, ast2) 262 && ast1.getColumnNo() < ast2.getColumnNo(); 263 } 264 265 /** 266 * Retrieves the names of the type parameters to the node. 267 * 268 * @param node the parameterized AST node 269 * @return a list of type parameter names 270 */ 271 public static List<String> getTypeParameterNames(final DetailAST node) { 272 final DetailAST typeParameters = 273 node.findFirstToken(TokenTypes.TYPE_PARAMETERS); 274 275 final List<String> typeParameterNames = new ArrayList<>(); 276 if (typeParameters != null) { 277 final DetailAST typeParam = 278 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER); 279 typeParameterNames.add( 280 typeParam.findFirstToken(TokenTypes.IDENT).getText()); 281 282 DetailAST sibling = typeParam.getNextSibling(); 283 while (sibling != null) { 284 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) { 285 typeParameterNames.add( 286 sibling.findFirstToken(TokenTypes.IDENT).getText()); 287 } 288 sibling = sibling.getNextSibling(); 289 } 290 } 291 292 return typeParameterNames; 293 } 294 295 /** 296 * Retrieves the type parameters to the node. 297 * 298 * @param node the parameterized AST node 299 * @return a list of type parameter names 300 */ 301 public static List<DetailAST> getTypeParameters(final DetailAST node) { 302 final DetailAST typeParameters = 303 node.findFirstToken(TokenTypes.TYPE_PARAMETERS); 304 305 final List<DetailAST> typeParams = new ArrayList<>(); 306 if (typeParameters != null) { 307 final DetailAST typeParam = 308 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER); 309 typeParams.add(typeParam); 310 311 DetailAST sibling = typeParam.getNextSibling(); 312 while (sibling != null) { 313 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) { 314 typeParams.add(sibling); 315 } 316 sibling = sibling.getNextSibling(); 317 } 318 } 319 320 return typeParams; 321 } 322 323 /** 324 * Returns whether an AST represents a setter method. 325 * 326 * @param ast the AST to check with 327 * @return whether the AST represents a setter method 328 */ 329 public static boolean isSetterMethod(final DetailAST ast) { 330 boolean setterMethod = false; 331 332 // Check have a method with exactly 7 children which are all that 333 // is allowed in a proper setter method which does not throw any 334 // exceptions. 335 if (ast.getType() == TokenTypes.METHOD_DEF 336 && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) { 337 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 338 final String name = type.getNextSibling().getText(); 339 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches(); 340 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null; 341 342 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 343 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1; 344 345 if (matchesSetterFormat && voidReturnType && singleParam) { 346 // Now verify that the body consists of: 347 // SLIST -> EXPR -> ASSIGN 348 // SEMI 349 // RCURLY 350 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 351 352 if (slist != null && slist.getChildCount() == SETTER_BODY_SIZE) { 353 final DetailAST expr = slist.getFirstChild(); 354 setterMethod = expr.getFirstChild().getType() == TokenTypes.ASSIGN; 355 } 356 } 357 } 358 return setterMethod; 359 } 360 361 /** 362 * Returns whether an AST represents a getter method. 363 * 364 * @param ast the AST to check with 365 * @return whether the AST represents a getter method 366 */ 367 public static boolean isGetterMethod(final DetailAST ast) { 368 boolean getterMethod = false; 369 370 // Check have a method with exactly 7 children which are all that 371 // is allowed in a proper getter method which does not throw any 372 // exceptions. 373 if (ast.getType() == TokenTypes.METHOD_DEF 374 && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) { 375 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 376 final String name = type.getNextSibling().getText(); 377 final boolean matchesGetterFormat = GETTER_PATTERN.matcher(name).matches(); 378 final boolean noVoidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) == null; 379 380 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 381 final boolean noParams = params.getChildCount(TokenTypes.PARAMETER_DEF) == 0; 382 383 if (matchesGetterFormat && noVoidReturnType && noParams) { 384 // Now verify that the body consists of: 385 // SLIST -> RETURN 386 // RCURLY 387 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 388 389 if (slist != null && slist.getChildCount() == GETTER_BODY_SIZE) { 390 final DetailAST expr = slist.getFirstChild(); 391 getterMethod = expr.getType() == TokenTypes.LITERAL_RETURN; 392 } 393 } 394 } 395 return getterMethod; 396 } 397 398 /** 399 * Checks whether a method is a not void one. 400 * 401 * @param methodDefAst the method node. 402 * @return true if method is a not void one. 403 */ 404 public static boolean isNonVoidMethod(DetailAST methodDefAst) { 405 boolean returnValue = false; 406 if (methodDefAst.getType() == TokenTypes.METHOD_DEF) { 407 final DetailAST typeAST = methodDefAst.findFirstToken(TokenTypes.TYPE); 408 if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) == null) { 409 returnValue = true; 410 } 411 } 412 return returnValue; 413 } 414 415 /** 416 * Checks whether a parameter is a receiver. 417 * 418 * @param parameterDefAst the parameter node. 419 * @return true if the parameter is a receiver. 420 */ 421 public static boolean isReceiverParameter(DetailAST parameterDefAst) { 422 return parameterDefAst.getType() == TokenTypes.PARAMETER_DEF 423 && parameterDefAst.findFirstToken(TokenTypes.IDENT) == null; 424 } 425 426 /** 427 * Returns the access modifier of the method/constructor at the specified AST. If 428 * the method is in an interface or annotation block, the access modifier is assumed 429 * to be public. 430 * 431 * @param ast the token of the method/constructor. 432 * @return the access modifier of the method/constructor. 433 */ 434 public static AccessModifierOption getAccessModifierFromModifiersToken(DetailAST ast) { 435 final DetailAST modsToken = ast.findFirstToken(TokenTypes.MODIFIERS); 436 AccessModifierOption accessModifier = 437 getAccessModifierFromModifiersTokenDirectly(modsToken); 438 439 if (accessModifier == AccessModifierOption.PACKAGE) { 440 if (ScopeUtil.isInEnumBlock(ast) && ast.getType() == TokenTypes.CTOR_DEF) { 441 accessModifier = AccessModifierOption.PRIVATE; 442 } 443 else if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) { 444 accessModifier = AccessModifierOption.PUBLIC; 445 } 446 } 447 448 return accessModifier; 449 } 450 451 /** 452 * Returns {@link AccessModifierOption} based on the information about access modifier 453 * taken from the given token of type {@link TokenTypes#MODIFIERS}. 454 * 455 * @param modifiersToken token of type {@link TokenTypes#MODIFIERS}. 456 * @return {@link AccessModifierOption}. 457 * @throws IllegalArgumentException when expected non-null modifiersToken with type 'MODIFIERS' 458 */ 459 private static AccessModifierOption getAccessModifierFromModifiersTokenDirectly( 460 DetailAST modifiersToken) { 461 if (modifiersToken == null) { 462 throw new IllegalArgumentException("expected non-null AST-token with type 'MODIFIERS'"); 463 } 464 465 AccessModifierOption accessModifier = AccessModifierOption.PACKAGE; 466 for (DetailAST token = modifiersToken.getFirstChild(); token != null; 467 token = token.getNextSibling()) { 468 final int tokenType = token.getType(); 469 if (tokenType == TokenTypes.LITERAL_PUBLIC) { 470 accessModifier = AccessModifierOption.PUBLIC; 471 } 472 else if (tokenType == TokenTypes.LITERAL_PROTECTED) { 473 accessModifier = AccessModifierOption.PROTECTED; 474 } 475 else if (tokenType == TokenTypes.LITERAL_PRIVATE) { 476 accessModifier = AccessModifierOption.PRIVATE; 477 } 478 } 479 return accessModifier; 480 } 481 482 /** 483 * Returns the access modifier of the surrounding "block". 484 * 485 * @param node the node to return the access modifier for 486 * @return the access modifier of the surrounding block 487 */ 488 public static AccessModifierOption getSurroundingAccessModifier(DetailAST node) { 489 AccessModifierOption returnValue = null; 490 for (DetailAST token = node.getParent(); 491 returnValue == null && !TokenUtil.isRootNode(token); 492 token = token.getParent()) { 493 final int type = token.getType(); 494 if (type == TokenTypes.CLASS_DEF 495 || type == TokenTypes.INTERFACE_DEF 496 || type == TokenTypes.ANNOTATION_DEF 497 || type == TokenTypes.ENUM_DEF) { 498 returnValue = getAccessModifierFromModifiersToken(token); 499 } 500 else if (type == TokenTypes.LITERAL_NEW) { 501 break; 502 } 503 } 504 505 return returnValue; 506 } 507 508 /** 509 * Create set of class names and short class names. 510 * 511 * @param classNames array of class names. 512 * @return set of class names and short class names. 513 */ 514 public static Set<String> parseClassNames(String... classNames) { 515 final Set<String> illegalClassNames = new HashSet<>(); 516 for (final String name : classNames) { 517 illegalClassNames.add(name); 518 final int lastDot = name.lastIndexOf('.'); 519 if (lastDot != -1 && lastDot < name.length() - 1) { 520 final String shortName = name 521 .substring(name.lastIndexOf('.') + 1); 522 illegalClassNames.add(shortName); 523 } 524 } 525 return illegalClassNames; 526 } 527 528 /** 529 * Strip initial newline and preceding whitespace on each line from text block content. 530 * In order to be consistent with how javac handles this task, we have modeled this 531 * implementation after the code from: 532 * github.com/openjdk/jdk14u/blob/master/src/java.base/share/classes/java/lang/String.java 533 * 534 * @param textBlockContent the actual content of the text block. 535 * @return string consistent with javac representation. 536 */ 537 public static String stripIndentAndInitialNewLineFromTextBlock(String textBlockContent) { 538 final String contentWithInitialNewLineRemoved = 539 ALL_NEW_LINES.matcher(textBlockContent).replaceFirst(""); 540 final List<String> lines = 541 Arrays.asList(ALL_NEW_LINES.split(contentWithInitialNewLineRemoved)); 542 final int indent = getSmallestIndent(lines); 543 final String suffix = ""; 544 545 return lines.stream() 546 .map(line -> stripIndentAndTrailingWhitespaceFromLine(line, indent)) 547 .collect(Collectors.joining(System.lineSeparator(), suffix, suffix)); 548 } 549 550 /** 551 * Helper method for stripIndentAndInitialNewLineFromTextBlock, strips correct indent 552 * from string, and trailing whitespace, or returns empty string if no text. 553 * 554 * @param line the string to strip indent and trailing whitespace from 555 * @param indent the amount of indent to remove 556 * @return modified string with removed indent and trailing whitespace, or empty string. 557 */ 558 private static String stripIndentAndTrailingWhitespaceFromLine(String line, int indent) { 559 final int lastNonWhitespace = lastIndexOfNonWhitespace(line); 560 String returnString = ""; 561 if (lastNonWhitespace > 0) { 562 returnString = line.substring(indent, lastNonWhitespace); 563 } 564 return returnString; 565 } 566 567 /** 568 * Helper method for stripIndentAndInitialNewLineFromTextBlock, to determine the smallest 569 * indent in a text block string literal. 570 * 571 * @param lines list of actual text block content, split by line. 572 * @return number of spaces representing the smallest indent in this text block. 573 */ 574 private static int getSmallestIndent(List<String> lines) { 575 return lines.stream() 576 .mapToInt(CommonUtil::indexOfNonWhitespace) 577 .min() 578 .orElse(0); 579 } 580 581 /** 582 * Helper method to find the index of the last non-whitespace character in a string. 583 * 584 * @param line the string to find the last index of a non-whitespace character for. 585 * @return the index of the last non-whitespace character. 586 */ 587 private static int lastIndexOfNonWhitespace(String line) { 588 int length; 589 for (length = line.length(); length > 0; length--) { 590 if (!Character.isWhitespace(line.charAt(length - 1))) { 591 break; 592 } 593 } 594 return length; 595 } 596 597 /** 598 * Calculates and returns the type declaration name matching count. 599 * 600 * <p> 601 * Suppose our pattern class is {@code foo.a.b} and class to be matched is 602 * {@code foo.a.ball} then type declaration name matching count would be calculated by 603 * comparing every character, and updating main counter when we hit "." to prevent matching 604 * "a.b" with "a.ball". In this case type declaration name matching count 605 * would be equal to 6 and not 7 (b of ball is not counted). 606 * </p> 607 * 608 * @param patternClass class against which the given class has to be matched 609 * @param classToBeMatched class to be matched 610 * @return class name matching count 611 */ 612 public static int typeDeclarationNameMatchingCount(String patternClass, 613 String classToBeMatched) { 614 final int length = Math.min(classToBeMatched.length(), patternClass.length()); 615 int result = 0; 616 for (int i = 0; i < length && patternClass.charAt(i) == classToBeMatched.charAt(i); ++i) { 617 if (patternClass.charAt(i) == PACKAGE_SEPARATOR) { 618 result = i; 619 } 620 } 621 return result; 622 } 623 624 /** 625 * Get the qualified name of type declaration by combining {@code packageName}, 626 * {@code outerClassQualifiedName} and {@code className}. 627 * 628 * @param packageName packageName 629 * @param outerClassQualifiedName outerClassQualifiedName 630 * @param className className 631 * @return the qualified name of type declaration by combining {@code packageName}, 632 * {@code outerClassQualifiedName} and {@code className} 633 */ 634 public static String getQualifiedTypeDeclarationName(String packageName, 635 String outerClassQualifiedName, 636 String className) { 637 final String qualifiedClassName; 638 639 if (outerClassQualifiedName == null) { 640 if (packageName == null) { 641 qualifiedClassName = className; 642 } 643 else { 644 qualifiedClassName = packageName + PACKAGE_SEPARATOR + className; 645 } 646 } 647 else { 648 qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className; 649 } 650 return qualifiedClassName; 651 } 652 653 /** 654 * Get name of package and super class of anon inner class by concatenating 655 * the identifier values under {@link TokenTypes#DOT}. 656 * 657 * @param ast ast to extract superclass or package name from 658 * @return qualified name 659 */ 660 public static String extractQualifiedName(DetailAST ast) { 661 return FullIdent.createFullIdent(ast).getText(); 662 } 663 664 /** 665 * Get the short name of super class of anonymous inner class. 666 * Example: 667 * <pre> 668 * TestClass.NestedClass obj = new Test().new NestedClass() {}; 669 * // Short name will be Test.NestedClass 670 * </pre> 671 * 672 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 673 * @return short name of base class of anonymous inner class 674 */ 675 public static String getShortNameOfAnonInnerClass(DetailAST literalNewAst) { 676 DetailAST parentAst = literalNewAst.getParent(); 677 while (TokenUtil.isOfType(parentAst, TokenTypes.LITERAL_NEW, TokenTypes.DOT)) { 678 parentAst = parentAst.getParent(); 679 } 680 final DetailAST firstChild = parentAst.getFirstChild(); 681 return extractQualifiedName(firstChild); 682 } 683}