1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.utils;
21
22 import java.io.Closeable;
23 import java.io.File;
24 import java.io.IOException;
25 import java.lang.reflect.Constructor;
26 import java.lang.reflect.InvocationTargetException;
27 import java.net.MalformedURLException;
28 import java.net.URI;
29 import java.net.URISyntaxException;
30 import java.net.URL;
31 import java.nio.file.Path;
32 import java.util.BitSet;
33 import java.util.Objects;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import java.util.regex.PatternSyntaxException;
37
38 import org.xml.sax.InputSource;
39
40 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
41
42 /**
43 * Contains utility methods.
44 *
45 */
46 public final class CommonUtil {
47
48 /** Default tab width for column reporting. */
49 public static final int DEFAULT_TAB_WIDTH = 8;
50
51 /** For cases where no tokens should be accepted. */
52 public static final BitSet EMPTY_BIT_SET = new BitSet();
53 /** Copied from org.apache.commons.lang3.ArrayUtils. */
54 public static final String[] EMPTY_STRING_ARRAY = new String[0];
55 /** Copied from org.apache.commons.lang3.ArrayUtils. */
56 public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
57 /** Copied from org.apache.commons.lang3.ArrayUtils. */
58 public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
59 /** Copied from org.apache.commons.lang3.ArrayUtils. */
60 public static final int[] EMPTY_INT_ARRAY = new int[0];
61 /** Copied from org.apache.commons.lang3.ArrayUtils. */
62 public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
63 /** Copied from org.apache.commons.lang3.ArrayUtils. */
64 public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
65 /** Pseudo URL protocol for loading from the class path. */
66 public static final String CLASSPATH_URL_PROTOCOL = "classpath:";
67
68 /** Prefix for the exception when unable to find resource. */
69 private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
70
71 /** The extension separator. */
72 private static final String EXTENSION_SEPARATOR = ".";
73
74 /** Stop instances being created. **/
75 private CommonUtil() {
76 }
77
78 /**
79 * Helper method to create a regular expression.
80 *
81 * @param pattern
82 * the pattern to match
83 * @return a created regexp object
84 * @throws IllegalArgumentException
85 * if unable to create Pattern object.
86 **/
87 public static Pattern createPattern(String pattern) {
88 return createPattern(pattern, 0);
89 }
90
91 /**
92 * Helper method to create a regular expression with a specific flags.
93 *
94 * @param pattern
95 * the pattern to match
96 * @param flags
97 * the flags to set
98 * @return a created regexp object
99 * @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 }