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.io.Closeable; 023import java.io.File; 024import java.io.IOException; 025import java.lang.reflect.Constructor; 026import java.lang.reflect.InvocationTargetException; 027import java.net.MalformedURLException; 028import java.net.URI; 029import java.net.URISyntaxException; 030import java.net.URL; 031import java.nio.file.Path; 032import java.util.BitSet; 033import java.util.Objects; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036import java.util.regex.PatternSyntaxException; 037 038import org.xml.sax.InputSource; 039 040import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 041 042/** 043 * Contains utility methods. 044 * 045 */ 046public final class CommonUtil { 047 048 /** Default tab width for column reporting. */ 049 public static final int DEFAULT_TAB_WIDTH = 8; 050 051 /** For cases where no tokens should be accepted. */ 052 public static final BitSet EMPTY_BIT_SET = new BitSet(); 053 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 054 public static final String[] EMPTY_STRING_ARRAY = new String[0]; 055 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 056 public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0]; 057 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 058 public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; 059 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 060 public static final int[] EMPTY_INT_ARRAY = new int[0]; 061 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 062 public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 063 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 064 public static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; 065 /** Pseudo URL protocol for loading from the class path. */ 066 public static final String CLASSPATH_URL_PROTOCOL = "classpath:"; 067 068 /** Prefix for the exception when unable to find resource. */ 069 private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: "; 070 071 /** The extension separator. */ 072 private static final String EXTENSION_SEPARATOR = "."; 073 074 /** Stop instances being created. **/ 075 private CommonUtil() { 076 } 077 078 /** 079 * Helper method to create a regular expression. 080 * 081 * @param pattern 082 * the pattern to match 083 * @return a created regexp object 084 * @throws IllegalArgumentException 085 * if unable to create Pattern object. 086 **/ 087 public static Pattern createPattern(String pattern) { 088 return createPattern(pattern, 0); 089 } 090 091 /** 092 * Helper method to create a regular expression with a specific flags. 093 * 094 * @param pattern 095 * the pattern to match 096 * @param flags 097 * the flags to set 098 * @return a created regexp object 099 * @throws IllegalArgumentException 100 * if unable to create Pattern object. 101 **/ 102 public static Pattern createPattern(String pattern, int flags) { 103 try { 104 return Pattern.compile(pattern, flags); 105 } 106 catch (final PatternSyntaxException exc) { 107 throw new IllegalArgumentException( 108 "Failed to initialise regular expression " + pattern, exc); 109 } 110 } 111 112 /** 113 * Returns whether the file extension matches what we are meant to process. 114 * 115 * @param file 116 * the file to be checked. 117 * @param fileExtensions 118 * files extensions, empty property in config makes it matches to all. 119 * @return whether there is a match. 120 */ 121 public static boolean matchesFileExtension(File file, String... fileExtensions) { 122 boolean result = false; 123 if (fileExtensions == null || fileExtensions.length == 0) { 124 result = true; 125 } 126 else { 127 // normalize extensions so all of them have a leading dot 128 final String[] withDotExtensions = new String[fileExtensions.length]; 129 for (int i = 0; i < fileExtensions.length; i++) { 130 final String extension = fileExtensions[i]; 131 if (extension.startsWith(EXTENSION_SEPARATOR)) { 132 withDotExtensions[i] = extension; 133 } 134 else { 135 withDotExtensions[i] = EXTENSION_SEPARATOR + extension; 136 } 137 } 138 139 final String fileName = file.getName(); 140 for (final String fileExtension : withDotExtensions) { 141 if (fileName.endsWith(fileExtension)) { 142 result = true; 143 break; 144 } 145 } 146 } 147 148 return result; 149 } 150 151 /** 152 * Returns whether the specified string contains only whitespace up to the specified index. 153 * 154 * @param index 155 * index to check up to 156 * @param line 157 * the line to check 158 * @return whether there is only whitespace 159 */ 160 public static boolean hasWhitespaceBefore(int index, String line) { 161 boolean result = true; 162 for (int i = 0; i < index; i++) { 163 if (!Character.isWhitespace(line.charAt(i))) { 164 result = false; 165 break; 166 } 167 } 168 return result; 169 } 170 171 /** 172 * Returns the length of a string ignoring all trailing whitespace. 173 * It is a pity that there is not a trim() like 174 * method that only removed the trailing whitespace. 175 * 176 * @param line 177 * the string to process 178 * @return the length of the string ignoring all trailing whitespace 179 **/ 180 public static int lengthMinusTrailingWhitespace(String line) { 181 int len = line.length(); 182 for (int i = len - 1; i >= 0; i--) { 183 if (!Character.isWhitespace(line.charAt(i))) { 184 break; 185 } 186 len--; 187 } 188 return len; 189 } 190 191 /** 192 * Returns the length of a String prefix with tabs expanded. 193 * Each tab is counted as the number of characters is 194 * takes to jump to the next tab stop. 195 * 196 * @param inputString 197 * the input String 198 * @param toIdx 199 * index in string (exclusive) where the calculation stops 200 * @param tabWidth 201 * the distance between tab stop position. 202 * @return the length of string.substring(0, toIdx) with tabs expanded. 203 */ 204 public static int lengthExpandedTabs(String inputString, 205 int toIdx, 206 int tabWidth) { 207 int len = 0; 208 for (int idx = 0; idx < toIdx; idx++) { 209 if (inputString.codePointAt(idx) == '\t') { 210 len = (len / tabWidth + 1) * tabWidth; 211 } 212 else { 213 len++; 214 } 215 } 216 return len; 217 } 218 219 /** 220 * Validates whether passed string is a valid pattern or not. 221 * 222 * @param pattern 223 * string to validate 224 * @return true if the pattern is valid false otherwise 225 */ 226 public static boolean isPatternValid(String pattern) { 227 boolean isValid = true; 228 try { 229 Pattern.compile(pattern); 230 } 231 catch (final PatternSyntaxException ignored) { 232 isValid = false; 233 } 234 return isValid; 235 } 236 237 /** 238 * Returns base class name from qualified name. 239 * 240 * @param type 241 * the fully qualified name. Cannot be null 242 * @return the base class name from a fully qualified name 243 */ 244 public static String baseClassName(String type) { 245 final int index = type.lastIndexOf('.'); 246 return type.substring(index + 1); 247 } 248 249 /** 250 * Constructs a relative path between base directory and a given path. 251 * 252 * @param baseDirectory 253 * the base path to which given path is relativized 254 * @param path 255 * the path to relativize against base directory 256 * @return the relative normalized path between base directory and 257 * path or path if base directory is null. 258 */ 259 public static String relativizePath(final String baseDirectory, final String path) { 260 final String resultPath; 261 if (baseDirectory == null) { 262 resultPath = path; 263 } 264 else { 265 final Path pathAbsolute = Path.of(path); 266 final Path pathBase = Path.of(baseDirectory); 267 resultPath = pathBase.relativize(pathAbsolute).toString(); 268 } 269 return resultPath; 270 } 271 272 /** 273 * Gets constructor of targetClass. 274 * 275 * @param <T> type of the target class object. 276 * @param targetClass 277 * from which constructor is returned 278 * @param parameterTypes 279 * of constructor 280 * @return constructor of targetClass 281 * @throws IllegalStateException if any exception occurs 282 * @see Class#getConstructor(Class[]) 283 */ 284 public static <T> Constructor<T> getConstructor(Class<T> targetClass, 285 Class<?>... parameterTypes) { 286 try { 287 return targetClass.getConstructor(parameterTypes); 288 } 289 catch (NoSuchMethodException exc) { 290 throw new IllegalStateException(exc); 291 } 292 } 293 294 /** 295 * Returns new instance of a class. 296 * 297 * @param <T> 298 * type of constructor 299 * @param constructor 300 * to invoke 301 * @param parameters 302 * to pass to constructor 303 * @return new instance of class 304 * @throws IllegalStateException if any exception occurs 305 * @see Constructor#newInstance(Object...) 306 */ 307 public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) { 308 try { 309 return constructor.newInstance(parameters); 310 } 311 catch (InstantiationException | IllegalAccessException | InvocationTargetException exc) { 312 throw new IllegalStateException(exc); 313 } 314 } 315 316 /** 317 * Closes a stream re-throwing IOException as IllegalStateException. 318 * 319 * @param closeable 320 * Closeable object 321 * @throws IllegalStateException when any IOException occurs 322 */ 323 public static void close(Closeable closeable) { 324 if (closeable != null) { 325 try { 326 closeable.close(); 327 } 328 catch (IOException exc) { 329 throw new IllegalStateException("Cannot close the stream", exc); 330 } 331 } 332 } 333 334 /** 335 * Creates an input source from a file. 336 * 337 * @param filename name of the file. 338 * @return input source. 339 * @throws CheckstyleException if an error occurs. 340 */ 341 public static InputSource sourceFromFilename(String filename) throws CheckstyleException { 342 // figure out if this is a File or a URL 343 final URI uri = getUriByFilename(filename); 344 return new InputSource(uri.toASCIIString()); 345 } 346 347 /** 348 * Resolve the specified filename to a URI. 349 * 350 * @param filename name of the file 351 * @return resolved file URI 352 * @throws CheckstyleException on failure 353 */ 354 public static URI getUriByFilename(String filename) throws CheckstyleException { 355 URI uri = getWebOrFileProtocolUri(filename); 356 357 if (uri == null) { 358 uri = getFilepathOrClasspathUri(filename); 359 } 360 361 return uri; 362 } 363 364 /** 365 * Resolves the specified filename containing 'http', 'https', 'ftp', 366 * and 'file' protocols (or any RFC 2396 compliant URL) to a URI. 367 * 368 * @param filename name of the file 369 * @return resolved file URI or null if URL is malformed or non-existent 370 * @noinspection deprecation 371 * @noinspectionreason Disabled until #17646 372 */ 373 public static URI getWebOrFileProtocolUri(String filename) { 374 URI uri; 375 try { 376 final URL url = new URL(filename); 377 uri = url.toURI(); 378 } 379 catch (URISyntaxException | MalformedURLException ignored) { 380 uri = null; 381 } 382 return uri; 383 } 384 385 /** 386 * Resolves the specified local filename, possibly with 'classpath:' 387 * protocol, to a URI. First we attempt to create a new file with 388 * given filename, then attempt to load file from class path. 389 * 390 * @param filename name of the file 391 * @return resolved file URI 392 * @throws CheckstyleException on failure 393 */ 394 private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException { 395 final URI uri; 396 final File file = new File(filename); 397 398 if (file.exists()) { 399 uri = file.toURI(); 400 } 401 else { 402 final int lastIndexOfClasspathProtocol; 403 if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) { 404 lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length(); 405 } 406 else { 407 lastIndexOfClasspathProtocol = 0; 408 } 409 uri = getResourceFromClassPath(filename 410 .substring(lastIndexOfClasspathProtocol)); 411 } 412 return uri; 413 } 414 415 /** 416 * Gets a resource from the classpath. 417 * 418 * @param filename name of file 419 * @return URI of file in classpath 420 * @throws CheckstyleException on failure 421 */ 422 public static URI getResourceFromClassPath(String filename) throws CheckstyleException { 423 final URL configUrl; 424 if (filename.charAt(0) == '/') { 425 configUrl = getCheckstyleResource(filename); 426 } 427 else { 428 configUrl = ClassLoader.getSystemResource(filename); 429 } 430 431 if (configUrl == null) { 432 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename); 433 } 434 435 final URI uri; 436 try { 437 uri = configUrl.toURI(); 438 } 439 catch (final URISyntaxException exc) { 440 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, exc); 441 } 442 443 return uri; 444 } 445 446 /** 447 * Finds a resource with a given name in the Checkstyle resource bundle. 448 * This method is intended only for internal use in Checkstyle tests for 449 * easy mocking to gain 100% coverage. 450 * 451 * @param name name of the desired resource 452 * @return URI of the resource 453 */ 454 public static URL getCheckstyleResource(String name) { 455 return CommonUtil.class.getResource(name); 456 } 457 458 /** 459 * Puts part of line, which matches regexp into given template 460 * on positions $n where 'n' is number of matched part in line. 461 * 462 * @param template the string to expand. 463 * @param lineToPlaceInTemplate contains expression which should be placed into string. 464 * @param regexp expression to find in comment. 465 * @return the string, based on template filled with given lines 466 */ 467 public static String fillTemplateWithStringsByRegexp( 468 String template, String lineToPlaceInTemplate, Pattern regexp) { 469 final Matcher matcher = regexp.matcher(lineToPlaceInTemplate); 470 String result = template; 471 if (matcher.find()) { 472 for (int i = 0; i <= matcher.groupCount(); i++) { 473 // $n expands comment match like in Pattern.subst(). 474 result = result.replaceAll("\\$" + i, matcher.group(i)); 475 } 476 } 477 return result; 478 } 479 480 /** 481 * Returns file name without extension. 482 * We do not use the method from Guava library to reduce Checkstyle's dependencies 483 * on external libraries. 484 * 485 * @param fullFilename file name with extension. 486 * @return file name without extension. 487 */ 488 public static String getFileNameWithoutExtension(String fullFilename) { 489 final String fileName = new File(fullFilename).getName(); 490 final int dotIndex = fileName.lastIndexOf('.'); 491 final String fileNameWithoutExtension; 492 if (dotIndex == -1) { 493 fileNameWithoutExtension = fileName; 494 } 495 else { 496 fileNameWithoutExtension = fileName.substring(0, dotIndex); 497 } 498 return fileNameWithoutExtension; 499 } 500 501 /** 502 * Returns file extension for the given file name 503 * or empty string if file does not have an extension. 504 * We do not use the method from Guava library to reduce Checkstyle's dependencies 505 * on external libraries. 506 * 507 * @param fileNameWithExtension file name with extension. 508 * @return file extension for the given file name 509 * or empty string if file does not have an extension. 510 */ 511 public static String getFileExtension(String fileNameWithExtension) { 512 final String fileName = Path.of(fileNameWithExtension).toString(); 513 final int dotIndex = fileName.lastIndexOf('.'); 514 final String extension; 515 if (dotIndex == -1) { 516 extension = ""; 517 } 518 else { 519 extension = fileName.substring(dotIndex + 1); 520 } 521 return extension; 522 } 523 524 /** 525 * Checks whether the given string is a valid identifier. 526 * 527 * @param str A string to check. 528 * @return true when the given string contains valid identifier. 529 */ 530 public static boolean isIdentifier(String str) { 531 boolean isIdentifier = !str.isEmpty(); 532 533 for (int i = 0; isIdentifier && i < str.length(); i++) { 534 if (i == 0) { 535 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0)); 536 } 537 else { 538 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i)); 539 } 540 } 541 542 return isIdentifier; 543 } 544 545 /** 546 * Checks whether the given string is a valid name. 547 * 548 * @param str A string to check. 549 * @return true when the given string contains valid name. 550 */ 551 public static boolean isName(String str) { 552 boolean isName = false; 553 554 final String[] identifiers = str.split("\\.", -1); 555 for (String identifier : identifiers) { 556 isName = isIdentifier(identifier); 557 if (!isName) { 558 break; 559 } 560 } 561 562 return isName; 563 } 564 565 /** 566 * Checks if the value arg is blank by either being null, 567 * empty, or contains only whitespace characters. 568 * 569 * @param value A string to check. 570 * @return true if the arg is blank. 571 */ 572 public static boolean isBlank(String value) { 573 return Objects.isNull(value) 574 || indexOfNonWhitespace(value) >= value.length(); 575 } 576 577 /** 578 * Method to find the index of the first non-whitespace character in a string. 579 * 580 * @param value the string to find the first index of a non-whitespace character for. 581 * @return the index of the first non-whitespace character. 582 */ 583 public static int indexOfNonWhitespace(String value) { 584 final int length = value.length(); 585 int left = 0; 586 while (left < length) { 587 final int codePointAt = value.codePointAt(left); 588 if (!Character.isWhitespace(codePointAt)) { 589 break; 590 } 591 left += Character.charCount(codePointAt); 592 } 593 return left; 594 } 595 596 /** 597 * Converts the Unicode code point at index {@code index} to it's UTF-16 598 * representation, then checks if the character is whitespace. Note that the given 599 * index {@code index} should correspond to the location of the character 600 * to check in the string, not in code points. 601 * 602 * @param codePoints the array of Unicode code points 603 * @param index the index of the character to check 604 * @return true if character at {@code index} is whitespace 605 */ 606 public static boolean isCodePointWhitespace(int[] codePoints, int index) { 607 // We only need to check the first member of a surrogate pair to verify that 608 // it is not whitespace. 609 final char character = Character.toChars(codePoints[index])[0]; 610 return Character.isWhitespace(character); 611 } 612 613}