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