View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.nio.file.Path;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.function.Predicate;
30  import java.util.regex.Pattern;
31  import java.util.stream.Collectors;
32  import java.util.stream.Stream;
33  
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.FullIdent;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
38  
39  /**
40   * Contains utility methods for the checks.
41   *
42   */
43  public final class CheckUtil {
44  
45      // constants for parseDouble()
46      /** Binary radix. */
47      private static final int BASE_2 = 2;
48  
49      /** Octal radix. */
50      private static final int BASE_8 = 8;
51  
52      /** Decimal radix. */
53      private static final int BASE_10 = 10;
54  
55      /** Hex radix. */
56      private static final int BASE_16 = 16;
57  
58      /** Pattern matching underscore characters ('_'). */
59      private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_");
60  
61      /** Compiled pattern for all system newlines. */
62      private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R");
63  
64      /** Package separator. */
65      private static final char PACKAGE_SEPARATOR = '.';
66  
67      /** Prevent instances. */
68      private CheckUtil() {
69      }
70  
71      /**
72       * Tests whether a method definition AST defines an equals covariant.
73       *
74       * @param ast the method definition AST to test.
75       *     Precondition: ast is a TokenTypes.METHOD_DEF node.
76       * @return true if ast defines an equals covariant.
77       */
78      public static boolean isEqualsMethod(DetailAST ast) {
79          boolean equalsMethod = false;
80  
81          if (ast.getType() == TokenTypes.METHOD_DEF) {
82              final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
83              final boolean staticOrAbstract =
84                      modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
85                      || modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
86  
87              if (!staticOrAbstract) {
88                  final DetailAST nameNode = ast.findFirstToken(TokenTypes.IDENT);
89                  final String name = nameNode.getText();
90  
91                  if ("equals".equals(name)) {
92                      // one parameter?
93                      final DetailAST paramsNode = ast.findFirstToken(TokenTypes.PARAMETERS);
94                      equalsMethod = paramsNode.getChildCount() == 1;
95                  }
96              }
97          }
98          return equalsMethod;
99      }
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 }