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