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.nio.file.Path;
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
112 return switch (type) {
113 case TokenTypes.NUM_FLOAT, TokenTypes.NUM_DOUBLE -> Double.parseDouble(txt);
114
115 case TokenTypes.NUM_INT, TokenTypes.NUM_LONG -> {
116 int radix = BASE_10;
117 if (txt.startsWith("0x") || txt.startsWith("0X")) {
118 radix = BASE_16;
119 txt = txt.substring(2);
120 }
121 else if (txt.startsWith("0b") || txt.startsWith("0B")) {
122 radix = BASE_2;
123 txt = txt.substring(2);
124 }
125 else if (txt.startsWith("0")) {
126 radix = BASE_8;
127 }
128 yield parseNumber(txt, radix, type);
129 }
130
131 default -> Double.NaN;
132 };
133 }
134
135 /**
136 * Parses the string argument as an integer or a long in the radix specified by
137 * the second argument. The characters in the string must all be digits of
138 * the specified radix.
139 *
140 * @param text the String containing the integer representation to be
141 * parsed. Precondition: text contains a parsable int.
142 * @param radix the radix to be used while parsing text.
143 * @param type the token type of the text. Should be a constant of
144 * {@link TokenTypes}.
145 * @return the number represented by the string argument in the specified radix.
146 */
147 private static double parseNumber(final String text, final int radix, final int type) {
148 String txt = text;
149 if (txt.endsWith("L") || txt.endsWith("l")) {
150 txt = txt.substring(0, txt.length() - 1);
151 }
152 final double result;
153
154 final boolean negative = txt.charAt(0) == '-';
155 if (type == TokenTypes.NUM_INT) {
156 if (negative) {
157 result = Integer.parseInt(txt, radix);
158 }
159 else {
160 result = Integer.parseUnsignedInt(txt, radix);
161 }
162 }
163 else {
164 if (negative) {
165 result = Long.parseLong(txt, radix);
166 }
167 else {
168 result = Long.parseUnsignedLong(txt, radix);
169 }
170 }
171
172 return result;
173 }
174
175 /**
176 * Finds sub-node for given node minimal (line, column) pair.
177 *
178 * @param node the root of tree for search.
179 * @return sub-node with minimal (line, column) pair.
180 */
181 public static DetailAST getFirstNode(final DetailAST node) {
182 DetailAST currentNode = node;
183 DetailAST child = node.getFirstChild();
184 while (child != null) {
185 final DetailAST newNode = getFirstNode(child);
186 if (isBeforeInSource(newNode, currentNode)) {
187 currentNode = newNode;
188 }
189 child = child.getNextSibling();
190 }
191
192 return currentNode;
193 }
194
195 /**
196 * Retrieves whether ast1 is located before ast2.
197 *
198 * @param ast1 the first node.
199 * @param ast2 the second node.
200 * @return true, if ast1 is located before ast2.
201 */
202 public static boolean isBeforeInSource(DetailAST ast1, DetailAST ast2) {
203 return ast1.getLineNo() < ast2.getLineNo()
204 || TokenUtil.areOnSameLine(ast1, ast2)
205 && ast1.getColumnNo() < ast2.getColumnNo();
206 }
207
208 /**
209 * Retrieves the names of the type parameters to the node.
210 *
211 * @param node the parameterized AST node
212 * @return a list of type parameter names
213 */
214 public static List<String> getTypeParameterNames(final DetailAST node) {
215 final DetailAST typeParameters =
216 node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
217
218 final List<String> typeParameterNames = new ArrayList<>();
219 if (typeParameters != null) {
220 final DetailAST typeParam =
221 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER);
222 typeParameterNames.add(
223 typeParam.findFirstToken(TokenTypes.IDENT).getText());
224
225 DetailAST sibling = typeParam.getNextSibling();
226 while (sibling != null) {
227 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) {
228 typeParameterNames.add(
229 sibling.findFirstToken(TokenTypes.IDENT).getText());
230 }
231 sibling = sibling.getNextSibling();
232 }
233 }
234
235 return typeParameterNames;
236 }
237
238 /**
239 * Retrieves the type parameters to the node.
240 *
241 * @param node the parameterized AST node
242 * @return a list of type parameter names
243 */
244 public static List<DetailAST> getTypeParameters(final DetailAST node) {
245 final DetailAST typeParameters =
246 node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
247
248 final List<DetailAST> typeParams = new ArrayList<>();
249 if (typeParameters != null) {
250 final DetailAST typeParam =
251 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER);
252 typeParams.add(typeParam);
253
254 DetailAST sibling = typeParam.getNextSibling();
255 while (sibling != null) {
256 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) {
257 typeParams.add(sibling);
258 }
259 sibling = sibling.getNextSibling();
260 }
261 }
262
263 return typeParams;
264 }
265
266 /**
267 * Checks whether a method is a not void one.
268 *
269 * @param methodDefAst the method node.
270 * @return true if method is a not void one.
271 */
272 public static boolean isNonVoidMethod(DetailAST methodDefAst) {
273 boolean returnValue = false;
274 if (methodDefAst.getType() == TokenTypes.METHOD_DEF) {
275 final DetailAST typeAST = methodDefAst.findFirstToken(TokenTypes.TYPE);
276 if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) == null) {
277 returnValue = true;
278 }
279 }
280 return returnValue;
281 }
282
283 /**
284 * Checks whether a parameter is a receiver.
285 *
286 * <p>A receiver parameter is a special parameter that
287 * represents the object for which the method is invoked.
288 * It is denoted by the reserved keyword {@code this}
289 * in the method declaration. Check
290 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF">
291 * PARAMETER_DEF</a>
292 * </p>
293 *
294 * @param parameterDefAst the parameter node.
295 * @return true if the parameter is a receiver.
296 * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.1">
297 * ReceiverParameter</a>
298 */
299 public static boolean isReceiverParameter(DetailAST parameterDefAst) {
300 return parameterDefAst.findFirstToken(TokenTypes.IDENT) == null;
301 }
302
303 /**
304 * Returns the access modifier of the method/constructor at the specified AST. If
305 * the method is in an interface or annotation block, the access modifier is assumed
306 * to be public.
307 *
308 * @param ast the token of the method/constructor.
309 * @return the access modifier of the method/constructor.
310 */
311 public static AccessModifierOption getAccessModifierFromModifiersToken(DetailAST ast) {
312 AccessModifierOption accessModifier;
313 if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
314 accessModifier = AccessModifierOption.PUBLIC;
315 }
316 else {
317 final DetailAST modsToken = ast.findFirstToken(TokenTypes.MODIFIERS);
318 accessModifier = getAccessModifierFromModifiersTokenDirectly(modsToken);
319 }
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 final Path filename = Path.of(filePath).getFileName();
567 return filename != null && "package-info.java".equals(filename.toString());
568 }
569}