View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.File;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.function.Predicate;
29  import java.util.regex.Pattern;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import com.puppycrawl.tools.checkstyle.api.DetailAST;
34  import com.puppycrawl.tools.checkstyle.api.FullIdent;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
37  
38  /**
39   * Contains utility methods for the checks.
40   *
41   */
42  public final class CheckUtil {
43  
44      // constants for parseDouble()
45      /** Binary radix. */
46      private static final int BASE_2 = 2;
47  
48      /** Octal radix. */
49      private static final int BASE_8 = 8;
50  
51      /** Decimal radix. */
52      private static final int BASE_10 = 10;
53  
54      /** Hex radix. */
55      private static final int BASE_16 = 16;
56  
57      /** Pattern matching underscore characters ('_'). */
58      private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_");
59  
60      /** Compiled pattern for all system newlines. */
61      private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R");
62  
63      /** Package separator. */
64      private static final char PACKAGE_SEPARATOR = '.';
65  
66      /** Prevent instances. */
67      private CheckUtil() {
68      }
69  
70      /**
71       * Tests whether a method definition AST defines an equals covariant.
72       *
73       * @param ast the method definition AST to test.
74       *     Precondition: ast is a TokenTypes.METHOD_DEF node.
75       * @return true if ast defines an equals covariant.
76       */
77      public static boolean isEqualsMethod(DetailAST ast) {
78          boolean equalsMethod = false;
79  
80          if (ast.getType() == TokenTypes.METHOD_DEF) {
81              final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
82              final boolean staticOrAbstract =
83                      modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
84                      || modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
85  
86              if (!staticOrAbstract) {
87                  final DetailAST nameNode = ast.findFirstToken(TokenTypes.IDENT);
88                  final String name = nameNode.getText();
89  
90                  if ("equals".equals(name)) {
91                      // one parameter?
92                      final DetailAST paramsNode = ast.findFirstToken(TokenTypes.PARAMETERS);
93                      equalsMethod = paramsNode.getChildCount() == 1;
94                  }
95              }
96          }
97          return equalsMethod;
98      }
99  
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         final double result;
112         switch (type) {
113             case TokenTypes.NUM_FLOAT:
114             case TokenTypes.NUM_DOUBLE:
115                 result = Double.parseDouble(txt);
116                 break;
117             case TokenTypes.NUM_INT:
118             case TokenTypes.NUM_LONG:
119                 int radix = BASE_10;
120                 if (txt.startsWith("0x") || txt.startsWith("0X")) {
121                     radix = BASE_16;
122                     txt = txt.substring(2);
123                 }
124                 else if (txt.startsWith("0b") || txt.startsWith("0B")) {
125                     radix = BASE_2;
126                     txt = txt.substring(2);
127                 }
128                 else if (txt.startsWith("0")) {
129                     radix = BASE_8;
130                 }
131                 result = parseNumber(txt, radix, type);
132                 break;
133             default:
134                 result = Double.NaN;
135                 break;
136         }
137         return result;
138     }
139 
140     /**
141      * Parses the string argument as an integer or a long in the radix specified by
142      * the second argument. The characters in the string must all be digits of
143      * the specified radix.
144      *
145      * @param text the String containing the integer representation to be
146      *     parsed. Precondition: text contains a parsable int.
147      * @param radix the radix to be used while parsing text.
148      * @param type the token type of the text. Should be a constant of
149      *     {@link TokenTypes}.
150      * @return the number represented by the string argument in the specified radix.
151      */
152     private static double parseNumber(final String text, final int radix, final int type) {
153         String txt = text;
154         if (txt.endsWith("L") || txt.endsWith("l")) {
155             txt = txt.substring(0, txt.length() - 1);
156         }
157         final double result;
158 
159         final boolean negative = txt.charAt(0) == '-';
160         if (type == TokenTypes.NUM_INT) {
161             if (negative) {
162                 result = Integer.parseInt(txt, radix);
163             }
164             else {
165                 result = Integer.parseUnsignedInt(txt, radix);
166             }
167         }
168         else {
169             if (negative) {
170                 result = Long.parseLong(txt, radix);
171             }
172             else {
173                 result = Long.parseUnsignedLong(txt, radix);
174             }
175         }
176 
177         return result;
178     }
179 
180     /**
181      * Finds sub-node for given node minimal (line, column) pair.
182      *
183      * @param node the root of tree for search.
184      * @return sub-node with minimal (line, column) pair.
185      */
186     public static DetailAST getFirstNode(final DetailAST node) {
187         DetailAST currentNode = node;
188         DetailAST child = node.getFirstChild();
189         while (child != null) {
190             final DetailAST newNode = getFirstNode(child);
191             if (isBeforeInSource(newNode, currentNode)) {
192                 currentNode = newNode;
193             }
194             child = child.getNextSibling();
195         }
196 
197         return currentNode;
198     }
199 
200     /**
201      * Retrieves whether ast1 is located before ast2.
202      *
203      * @param ast1 the first node.
204      * @param ast2 the second node.
205      * @return true, if ast1 is located before ast2.
206      */
207     public static boolean isBeforeInSource(DetailAST ast1, DetailAST ast2) {
208         return ast1.getLineNo() < ast2.getLineNo()
209             || TokenUtil.areOnSameLine(ast1, ast2)
210                 && ast1.getColumnNo() < ast2.getColumnNo();
211     }
212 
213     /**
214      * Retrieves the names of the type parameters to the node.
215      *
216      * @param node the parameterized AST node
217      * @return a list of type parameter names
218      */
219     public static List<String> getTypeParameterNames(final DetailAST node) {
220         final DetailAST typeParameters =
221             node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
222 
223         final List<String> typeParameterNames = new ArrayList<>();
224         if (typeParameters != null) {
225             final DetailAST typeParam =
226                 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER);
227             typeParameterNames.add(
228                     typeParam.findFirstToken(TokenTypes.IDENT).getText());
229 
230             DetailAST sibling = typeParam.getNextSibling();
231             while (sibling != null) {
232                 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) {
233                     typeParameterNames.add(
234                             sibling.findFirstToken(TokenTypes.IDENT).getText());
235                 }
236                 sibling = sibling.getNextSibling();
237             }
238         }
239 
240         return typeParameterNames;
241     }
242 
243     /**
244      * Retrieves the type parameters to the node.
245      *
246      * @param node the parameterized AST node
247      * @return a list of type parameter names
248      */
249     public static List<DetailAST> getTypeParameters(final DetailAST node) {
250         final DetailAST typeParameters =
251             node.findFirstToken(TokenTypes.TYPE_PARAMETERS);
252 
253         final List<DetailAST> typeParams = new ArrayList<>();
254         if (typeParameters != null) {
255             final DetailAST typeParam =
256                 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER);
257             typeParams.add(typeParam);
258 
259             DetailAST sibling = typeParam.getNextSibling();
260             while (sibling != null) {
261                 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) {
262                     typeParams.add(sibling);
263                 }
264                 sibling = sibling.getNextSibling();
265             }
266         }
267 
268         return typeParams;
269     }
270 
271     /**
272      * Checks whether a method is a not void one.
273      *
274      * @param methodDefAst the method node.
275      * @return true if method is a not void one.
276      */
277     public static boolean isNonVoidMethod(DetailAST methodDefAst) {
278         boolean returnValue = false;
279         if (methodDefAst.getType() == TokenTypes.METHOD_DEF) {
280             final DetailAST typeAST = methodDefAst.findFirstToken(TokenTypes.TYPE);
281             if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) == null) {
282                 returnValue = true;
283             }
284         }
285         return returnValue;
286     }
287 
288     /**
289      * Checks whether a parameter is a receiver.
290      *
291      * <p>A receiver parameter is a special parameter that
292      * represents the object for which the method is invoked.
293      * It is denoted by the reserved keyword {@code this}
294      * in the method declaration. Check
295      * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#PARAMETER_DEF">
296      * PARAMETER_DEF</a>
297      * </p>
298      *
299      * @param parameterDefAst the parameter node.
300      * @return true if the parameter is a receiver.
301      * @see <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.1">
302      *     ReceiverParameter</a>
303      */
304     public static boolean isReceiverParameter(DetailAST parameterDefAst) {
305         return parameterDefAst.findFirstToken(TokenTypes.IDENT) == null;
306     }
307 
308     /**
309      * Returns the access modifier of the method/constructor at the specified AST. If
310      * the method is in an interface or annotation block, the access modifier is assumed
311      * to be public.
312      *
313      * @param ast the token of the method/constructor.
314      * @return the access modifier of the method/constructor.
315      */
316     public static AccessModifierOption getAccessModifierFromModifiersToken(DetailAST ast) {
317         final DetailAST modsToken = ast.findFirstToken(TokenTypes.MODIFIERS);
318         AccessModifierOption accessModifier =
319                 getAccessModifierFromModifiersTokenDirectly(modsToken);
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         return "package-info.java".equals(new File(filePath).getName());
567     }
568 }