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.checks.design;
21  
22  import java.util.ArrayDeque;
23  import java.util.Comparator;
24  import java.util.Deque;
25  import java.util.HashMap;
26  import java.util.LinkedHashMap;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.function.Function;
30  import java.util.function.ToIntFunction;
31  
32  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
33  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
39  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
40  
41  /**
42   * <div>
43   * Ensures that identifies classes that can be effectively declared as final are explicitly
44   * marked as final. The following are different types of classes that can be identified:
45   * </div>
46   * <ol>
47   *   <li>
48   *       Private classes with no declared constructors.
49   *   </li>
50   *   <li>
51   *       Classes with any modifier, and contains only private constructors.
52   *   </li>
53   * </ol>
54   *
55   * <p>
56   * Classes are skipped if:
57   * </p>
58   * <ol>
59   *   <li>
60   *       Class is Super class of some Anonymous inner class.
61   *   </li>
62   *   <li>
63   *       Class is extended by another class in the same file.
64   *   </li>
65   * </ol>
66   *
67   * @since 3.1
68   */
69  @FileStatefulCheck
70  public class FinalClassCheck
71      extends AbstractCheck {
72  
73      /**
74       * A key is pointing to the warning message text in "messages.properties"
75       * file.
76       */
77      public static final String MSG_KEY = "final.class";
78  
79      /**
80       * Character separate package names in qualified name of java class.
81       */
82      private static final String PACKAGE_SEPARATOR = ".";
83  
84      /** Keeps ClassDesc objects for all inner classes. */
85      private Map<String, ClassDesc> innerClasses;
86  
87      /**
88       * Maps anonymous inner class's {@link TokenTypes#LITERAL_NEW} node to
89       * the outer type declaration's fully qualified name.
90       */
91      private Map<DetailAST, String> anonInnerClassToOuterTypeDecl;
92  
93      /** Keeps TypeDeclarationDescription object for stack of declared type descriptions. */
94      private Deque<TypeDeclarationDescription> typeDeclarations;
95  
96      /** Full qualified name of the package. */
97      private String packageName;
98  
99      @Override
100     public int[] getDefaultTokens() {
101         return getRequiredTokens();
102     }
103 
104     @Override
105     public int[] getAcceptableTokens() {
106         return getRequiredTokens();
107     }
108 
109     @Override
110     public int[] getRequiredTokens() {
111         return new int[] {
112             TokenTypes.ANNOTATION_DEF,
113             TokenTypes.CLASS_DEF,
114             TokenTypes.ENUM_DEF,
115             TokenTypes.INTERFACE_DEF,
116             TokenTypes.RECORD_DEF,
117             TokenTypes.CTOR_DEF,
118             TokenTypes.PACKAGE_DEF,
119             TokenTypes.LITERAL_NEW,
120         };
121     }
122 
123     @Override
124     public void beginTree(DetailAST rootAST) {
125         typeDeclarations = new ArrayDeque<>();
126         innerClasses = new LinkedHashMap<>();
127         anonInnerClassToOuterTypeDecl = new HashMap<>();
128         packageName = "";
129     }
130 
131     @Override
132     public void visitToken(DetailAST ast) {
133         switch (ast.getType()) {
134             case TokenTypes.PACKAGE_DEF ->
135                 packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling());
136 
137             case TokenTypes.ANNOTATION_DEF,
138                  TokenTypes.ENUM_DEF,
139                  TokenTypes.INTERFACE_DEF,
140                  TokenTypes.RECORD_DEF -> {
141                 final TypeDeclarationDescription description = new TypeDeclarationDescription(
142                     extractQualifiedTypeName(ast), 0, ast);
143                 typeDeclarations.push(description);
144             }
145 
146             case TokenTypes.CLASS_DEF -> visitClass(ast);
147 
148             case TokenTypes.CTOR_DEF -> visitCtor(ast);
149 
150             case TokenTypes.LITERAL_NEW -> {
151                 if (ast.getFirstChild() != null
152                         && ast.getLastChild().getType() == TokenTypes.OBJBLOCK) {
153 
154                     String outerTypeName = packageName;
155                     if (!typeDeclarations.isEmpty()) {
156                         outerTypeName = typeDeclarations.peek().getQualifiedName();
157                     }
158                     anonInnerClassToOuterTypeDecl.put(ast, outerTypeName);
159                 }
160             }
161 
162             default -> throw new IllegalStateException(ast.toString());
163         }
164     }
165 
166     /**
167      * Called to process a type definition.
168      *
169      * @param ast the token to process
170      */
171     private void visitClass(DetailAST ast) {
172         final String qualifiedClassName = extractQualifiedTypeName(ast);
173         final ClassDesc currClass = new ClassDesc(qualifiedClassName, typeDeclarations.size(), ast);
174         typeDeclarations.push(currClass);
175         innerClasses.put(qualifiedClassName, currClass);
176     }
177 
178     /**
179      * Called to process a constructor definition.
180      *
181      * @param ast the token to process
182      */
183     private void visitCtor(DetailAST ast) {
184         if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) {
185             final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
186             if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
187                 // Can be only of type ClassDesc, preceding if statements guarantee it.
188                 final ClassDesc desc = (ClassDesc) typeDeclarations.getFirst();
189                 desc.registerNonPrivateCtor();
190             }
191         }
192     }
193 
194     @Override
195     public void leaveToken(DetailAST ast) {
196         if (TokenUtil.isTypeDeclaration(ast.getType())) {
197             typeDeclarations.pop();
198         }
199     }
200 
201     @Override
202     public void finishTree(DetailAST rootAST) {
203         anonInnerClassToOuterTypeDecl.forEach(this::registerAnonymousInnerClassToSuperClass);
204         innerClasses.forEach(this::registerExtendedClass);
205         innerClasses.forEach((qualifiedClassName, classDesc) -> {
206             if (shouldBeDeclaredAsFinal(classDesc)) {
207                 final String className = CommonUtil.baseClassName(qualifiedClassName);
208                 log(classDesc.getTypeDeclarationAst(), MSG_KEY, className);
209             }
210         });
211     }
212 
213     /**
214      * Checks whether a class should be declared as final or not.
215      *
216      * @param classDesc description of the class
217      * @return true if given class should be declared as final otherwise false
218      */
219     private static boolean shouldBeDeclaredAsFinal(ClassDesc classDesc) {
220         final boolean shouldBeFinal;
221 
222         final boolean skipClass = classDesc.isDeclaredAsFinal()
223                     || classDesc.isDeclaredAsAbstract()
224                     || classDesc.isSuperClassOfAnonymousInnerClass()
225                     || classDesc.isWithNestedSubclass();
226 
227         if (skipClass) {
228             shouldBeFinal = false;
229         }
230         else if (classDesc.isHasDeclaredConstructor()) {
231             shouldBeFinal = classDesc.isDeclaredAsPrivate();
232         }
233         else {
234             shouldBeFinal = !classDesc.isWithNonPrivateCtor();
235         }
236         return shouldBeFinal;
237     }
238 
239     /**
240      * Register to outer super class of given classAst that
241      * given classAst is extending them.
242      *
243      * @param qualifiedClassName qualifies class name(with package) of the current class
244      * @param currentClass class which outer super class will be informed about nesting subclass
245      */
246     private void registerExtendedClass(String qualifiedClassName,
247                                        ClassDesc currentClass) {
248         final String superClassName = getSuperClassName(currentClass.getTypeDeclarationAst());
249         if (superClassName != null) {
250             final ToIntFunction<ClassDesc> nestedClassCountProvider = classDesc -> {
251                 return CheckUtil.typeDeclarationNameMatchingCount(qualifiedClassName,
252                                                                   classDesc.getQualifiedName());
253             };
254             getNearestClassWithSameName(superClassName, nestedClassCountProvider)
255                 .or(() -> Optional.ofNullable(innerClasses.get(superClassName)))
256                 .ifPresent(ClassDesc::registerNestedSubclass);
257         }
258     }
259 
260     /**
261      * Register to the super class of anonymous inner class that the given class is instantiated
262      * by an anonymous inner class.
263      *
264      * @param literalNewAst ast node of {@link TokenTypes#LITERAL_NEW} representing anonymous inner
265      *                      class
266      * @param outerTypeDeclName Fully qualified name of the outer type declaration of anonymous
267      *                          inner class
268      */
269     private void registerAnonymousInnerClassToSuperClass(DetailAST literalNewAst,
270                                                          String outerTypeDeclName) {
271         final String superClassName = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst);
272 
273         final ToIntFunction<ClassDesc> anonClassCountProvider = classDesc -> {
274             return getAnonSuperTypeMatchingCount(outerTypeDeclName, classDesc.getQualifiedName());
275         };
276         getNearestClassWithSameName(superClassName, anonClassCountProvider)
277             .or(() -> Optional.ofNullable(innerClasses.get(superClassName)))
278             .ifPresent(ClassDesc::registerSuperClassOfAnonymousInnerClass);
279     }
280 
281     /**
282      * Get the nearest class with same name.
283      *
284      * <p>The parameter {@code countProvider} exists because if the class being searched is the
285      * super class of anonymous inner class, the rules of evaluation are a bit different,
286      * consider the following example-
287      * <pre>
288      * {@code
289      * public class Main {
290      *     static class One {
291      *         static class Two {
292      *         }
293      *     }
294      *
295      *     class Three {
296      *         One.Two object = new One.Two() { // Object of Main.Three.One.Two
297      *                                          // and not of Main.One.Two
298      *         };
299      *
300      *         static class One {
301      *             static class Two {
302      *             }
303      *         }
304      *     }
305      * }
306      * }
307      * </pre>
308      * If the {@link Function} {@code countProvider} hadn't used
309      * {@link FinalClassCheck#getAnonSuperTypeMatchingCount} to
310      * calculate the matching count then the logic would have falsely evaluated
311      * {@code Main.One.Two} to be the super class of the anonymous inner class.
312      *
313      * @param className name of the class
314      * @param countProvider the function to apply to calculate the name matching count
315      * @return {@link Optional} of {@link ClassDesc} object of the nearest class with the same name.
316      * @noinspection CallToStringConcatCanBeReplacedByOperator
317      * @noinspectionreason CallToStringConcatCanBeReplacedByOperator - operator causes
318      *      pitest to fail
319      */
320     private Optional<ClassDesc> getNearestClassWithSameName(String className,
321         ToIntFunction<ClassDesc> countProvider) {
322         final String dotAndClassName = PACKAGE_SEPARATOR.concat(className);
323         final Comparator<ClassDesc> longestMatch = Comparator.comparingInt(countProvider);
324         return innerClasses.entrySet().stream()
325                 .filter(entry -> entry.getKey().endsWith(dotAndClassName))
326                 .map(Map.Entry::getValue)
327                 .min(longestMatch.reversed().thenComparingInt(ClassDesc::getDepth));
328     }
329 
330     /**
331      * Extract the qualified type declaration name from given type declaration Ast.
332      *
333      * @param typeDeclarationAst type declaration for which qualified name is being fetched
334      * @return qualified name of a type declaration
335      */
336     private String extractQualifiedTypeName(DetailAST typeDeclarationAst) {
337         final String className = typeDeclarationAst.findFirstToken(TokenTypes.IDENT).getText();
338         String outerTypeDeclarationQualifiedName = null;
339         if (!typeDeclarations.isEmpty()) {
340             outerTypeDeclarationQualifiedName = typeDeclarations.peek().getQualifiedName();
341         }
342         return CheckUtil.getQualifiedTypeDeclarationName(packageName,
343                                                          outerTypeDeclarationQualifiedName,
344                                                          className);
345     }
346 
347     /**
348      * Get super class name of given class.
349      *
350      * @param classAst class
351      * @return super class name or null if super class is not specified
352      */
353     private static String getSuperClassName(DetailAST classAst) {
354         String superClassName = null;
355         final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
356         if (classExtend != null) {
357             superClassName = CheckUtil.extractQualifiedName(classExtend.getFirstChild());
358         }
359         return superClassName;
360     }
361 
362     /**
363      * Calculates and returns the type declaration matching count when {@code classToBeMatched} is
364      * considered to be super class of an anonymous inner class.
365      *
366      * <p>
367      * Suppose our pattern class is {@code Main.ClassOne} and class to be matched is
368      * {@code Main.ClassOne.ClassTwo.ClassThree} then type declaration name matching count would
369      * be calculated by comparing every character, and updating main counter when we hit "." or
370      * when it is the last character of the pattern class and certain conditions are met. This is
371      * done so that matching count is 13 instead of 5. This is due to the fact that pattern class
372      * can contain anonymous inner class object of a nested class which isn't true in case of
373      * extending classes as you can't extend nested classes.
374      * </p>
375      *
376      * @param patternTypeDeclaration type declaration against which the given type declaration has
377      *                               to be matched
378      * @param typeDeclarationToBeMatched type declaration to be matched
379      * @return type declaration matching count
380      */
381     private static int getAnonSuperTypeMatchingCount(String patternTypeDeclaration,
382                                                     String typeDeclarationToBeMatched) {
383         final int typeDeclarationToBeMatchedLength = typeDeclarationToBeMatched.length();
384         final int minLength = Math
385             .min(typeDeclarationToBeMatchedLength, patternTypeDeclaration.length());
386         final char packageSeparator = PACKAGE_SEPARATOR.charAt(0);
387         final boolean shouldCountBeUpdatedAtLastCharacter =
388             typeDeclarationToBeMatchedLength > minLength
389                 && typeDeclarationToBeMatched.charAt(minLength) == packageSeparator;
390 
391         int result = 0;
392         for (int idx = 0;
393              idx < minLength
394                  && patternTypeDeclaration.charAt(idx) == typeDeclarationToBeMatched.charAt(idx);
395              idx++) {
396 
397             if (idx == minLength - 1 && shouldCountBeUpdatedAtLastCharacter
398                 || patternTypeDeclaration.charAt(idx) == packageSeparator) {
399                 result = idx;
400             }
401         }
402         return result;
403     }
404 
405     /**
406      * Maintains information about the type of declaration.
407      * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF}
408      * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF}
409      * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration.
410      * It does not maintain information about classes, a subclass called {@link ClassDesc}
411      * does that job.
412      */
413     private static class TypeDeclarationDescription {
414 
415         /**
416          * Complete type declaration name with package name and outer type declaration name.
417          */
418         private final String qualifiedName;
419 
420         /**
421          * Depth of nesting of type declaration.
422          */
423         private final int depth;
424 
425         /**
426          * Type declaration ast node.
427          */
428         private final DetailAST typeDeclarationAst;
429 
430         /**
431          * Create an instance of TypeDeclarationDescription.
432          *
433          * @param qualifiedName Complete type declaration name with package name and outer type
434          *                      declaration name.
435          * @param depth Depth of nesting of type declaration
436          * @param typeDeclarationAst Type declaration ast node
437          */
438         private TypeDeclarationDescription(String qualifiedName, int depth,
439                                           DetailAST typeDeclarationAst) {
440             this.qualifiedName = qualifiedName;
441             this.depth = depth;
442             this.typeDeclarationAst = typeDeclarationAst;
443         }
444 
445         /**
446          * Get the complete type declaration name i.e. type declaration name with package name
447          * and outer type declaration name.
448          *
449          * @return qualified class name
450          */
451         /* package */ String getQualifiedName() {
452             return qualifiedName;
453         }
454 
455         /**
456          * Get the depth of type declaration.
457          *
458          * @return the depth of nesting of type declaration
459          */
460         /* package */ int getDepth() {
461             return depth;
462         }
463 
464         /**
465          * Get the type declaration ast node.
466          *
467          * @return ast node of the type declaration
468          */
469 
470         /* package */ DetailAST getTypeDeclarationAst() {
471             return typeDeclarationAst;
472         }
473     }
474 
475     /**
476      * Maintains information about the class.
477      */
478     private static final class ClassDesc extends TypeDeclarationDescription {
479 
480         /** Is class declared as final. */
481         private final boolean declaredAsFinal;
482 
483         /** Is class declared as abstract. */
484         private final boolean declaredAsAbstract;
485 
486         /** Is class contains private modifier. */
487         private final boolean declaredAsPrivate;
488 
489         /** Does class have implicit constructor. */
490         private final boolean hasDeclaredConstructor;
491 
492         /** Does class have non-private ctors. */
493         private boolean withNonPrivateCtor;
494 
495         /** Does class have nested subclass. */
496         private boolean withNestedSubclass;
497 
498         /** Whether the class is the super class of an anonymous inner class. */
499         private boolean superClassOfAnonymousInnerClass;
500 
501         /**
502          *  Create a new ClassDesc instance.
503          *
504          *  @param qualifiedName qualified class name(with package)
505          *  @param depth class nesting level
506          *  @param classAst classAst node
507          */
508         private ClassDesc(String qualifiedName, int depth, DetailAST classAst) {
509             super(qualifiedName, depth, classAst);
510             final DetailAST modifiers = classAst.findFirstToken(TokenTypes.MODIFIERS);
511             declaredAsFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
512             declaredAsAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
513             declaredAsPrivate = modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
514             hasDeclaredConstructor =
515                     classAst.getLastChild().findFirstToken(TokenTypes.CTOR_DEF) == null;
516         }
517 
518         /** Adds non-private ctor. */
519         private void registerNonPrivateCtor() {
520             withNonPrivateCtor = true;
521         }
522 
523         /** Adds nested subclass. */
524         private void registerNestedSubclass() {
525             withNestedSubclass = true;
526         }
527 
528         /** Adds anonymous inner class. */
529         private void registerSuperClassOfAnonymousInnerClass() {
530             superClassOfAnonymousInnerClass = true;
531         }
532 
533         /**
534          *  Does class have non-private ctors.
535          *
536          *  @return true if class has non-private ctors
537          */
538         private boolean isWithNonPrivateCtor() {
539             return withNonPrivateCtor;
540         }
541 
542         /**
543          * Does class have nested subclass.
544          *
545          * @return true if class has nested subclass
546          */
547         private boolean isWithNestedSubclass() {
548             return withNestedSubclass;
549         }
550 
551         /**
552          *  Is class declared as final.
553          *
554          *  @return true if class is declared as final
555          */
556         private boolean isDeclaredAsFinal() {
557             return declaredAsFinal;
558         }
559 
560         /**
561          *  Is class declared as abstract.
562          *
563          *  @return true if class is declared as final
564          */
565         private boolean isDeclaredAsAbstract() {
566             return declaredAsAbstract;
567         }
568 
569         /**
570          * Whether the class is the super class of an anonymous inner class.
571          *
572          * @return {@code true} if the class is the super class of an anonymous inner class.
573          */
574         private boolean isSuperClassOfAnonymousInnerClass() {
575             return superClassOfAnonymousInnerClass;
576         }
577 
578         /**
579          * Does class have implicit constructor.
580          *
581          * @return true if class have implicit constructor
582          */
583         private boolean isHasDeclaredConstructor() {
584             return hasDeclaredConstructor;
585         }
586 
587         /**
588          * Does class is private.
589          *
590          * @return true if class is private
591          */
592         private boolean isDeclaredAsPrivate() {
593             return declaredAsPrivate;
594         }
595     }
596 }