View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.Deque;
24  import java.util.LinkedList;
25  import java.util.List;
26  
27  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
28  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.ScopeUtils;
32  
33  /**
34   * <p>
35   * Checks that class which has only private ctors
36   * is declared as final. Doesn't check for classes nested in interfaces
37   * or annotations, as they are always {@code final} there.
38   * </p>
39   * <p>
40   * An example of how to configure the check is:
41   * </p>
42   * <pre>
43   * &lt;module name="FinalClass"/&gt;
44   * </pre>
45   * @author o_sukhodolsky
46   */
47  @FileStatefulCheck
48  public class FinalClassCheck
49      extends AbstractCheck {
50  
51      /**
52       * A key is pointing to the warning message text in "messages.properties"
53       * file.
54       */
55      public static final String MSG_KEY = "final.class";
56  
57      /**
58       * Character separate package names in qualified name of java class.
59       */
60      private static final String PACKAGE_SEPARATOR = ".";
61  
62      /** Keeps ClassDesc objects for stack of declared classes. */
63      private Deque<ClassDesc> classes;
64  
65      /** Full qualified name of the package. */
66      private String packageName;
67  
68      @Override
69      public int[] getDefaultTokens() {
70          return getRequiredTokens();
71      }
72  
73      @Override
74      public int[] getAcceptableTokens() {
75          return getRequiredTokens();
76      }
77  
78      @Override
79      public int[] getRequiredTokens() {
80          return new int[] {TokenTypes.CLASS_DEF, TokenTypes.CTOR_DEF, TokenTypes.PACKAGE_DEF};
81      }
82  
83      @Override
84      public void beginTree(DetailAST rootAST) {
85          classes = new ArrayDeque<>();
86          packageName = "";
87      }
88  
89      @Override
90      public void visitToken(DetailAST ast) {
91          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
92  
93          switch (ast.getType()) {
94  
95              case TokenTypes.PACKAGE_DEF:
96                  packageName = extractQualifiedName(ast);
97                  break;
98  
99              case TokenTypes.CLASS_DEF:
100                 registerNestedSubclassToOuterSuperClasses(ast);
101 
102                 final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null;
103                 final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
104 
105                 final String qualifiedClassName = getQualifiedClassName(ast);
106                 classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract));
107                 break;
108 
109             case TokenTypes.CTOR_DEF:
110                 if (!ScopeUtils.isInEnumBlock(ast)) {
111                     final ClassDesc desc = classes.peek();
112                     if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) {
113                         desc.registerNonPrivateCtor();
114                     }
115                     else {
116                         desc.registerPrivateCtor();
117                     }
118                 }
119                 break;
120 
121             default:
122                 throw new IllegalStateException(ast.toString());
123         }
124     }
125 
126     @Override
127     public void leaveToken(DetailAST ast) {
128         if (ast.getType() == TokenTypes.CLASS_DEF) {
129             final ClassDesc desc = classes.pop();
130             if (desc.isWithPrivateCtor()
131                 && !desc.isDeclaredAsAbstract()
132                 && !desc.isDeclaredAsFinal()
133                 && !desc.isWithNonPrivateCtor()
134                 && !desc.isWithNestedSubclass()
135                 && !ScopeUtils.isInInterfaceOrAnnotationBlock(ast)) {
136                 final String qualifiedName = desc.getQualifiedName();
137                 final String className = getClassNameFromQualifiedName(qualifiedName);
138                 log(ast.getLineNo(), MSG_KEY, className);
139             }
140         }
141     }
142 
143     /**
144      * Get name of class(with qualified package if specified) in extend clause.
145      * @param classExtend extend clause to extract class name
146      * @return super class name
147      */
148     private static String extractQualifiedName(DetailAST classExtend) {
149         final String className;
150 
151         if (classExtend.findFirstToken(TokenTypes.IDENT) == null) {
152             // Name specified with packages, have to traverse DOT
153             final DetailAST firstChild = classExtend.findFirstToken(TokenTypes.DOT);
154             final List<String> qualifiedNameParts = new LinkedList<>();
155 
156             qualifiedNameParts.add(0, firstChild.findFirstToken(TokenTypes.IDENT).getText());
157             DetailAST traverse = firstChild.findFirstToken(TokenTypes.DOT);
158             while (traverse != null) {
159                 qualifiedNameParts.add(0, traverse.findFirstToken(TokenTypes.IDENT).getText());
160                 traverse = traverse.findFirstToken(TokenTypes.DOT);
161             }
162             className = String.join(PACKAGE_SEPARATOR, qualifiedNameParts);
163         }
164         else {
165             className = classExtend.findFirstToken(TokenTypes.IDENT).getText();
166         }
167 
168         return className;
169     }
170 
171     /**
172      * Register to outer super classes of given classAst that
173      * given classAst is extending them.
174      * @param classAst class which outer super classes will be
175      *                 informed about nesting subclass
176      */
177     private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) {
178         final String currentAstSuperClassName = getSuperClassName(classAst);
179         if (currentAstSuperClassName != null) {
180             for (ClassDesc classDesc : classes) {
181                 final String classDescQualifiedName = classDesc.getQualifiedName();
182                 if (doesNameInExtendMatchSuperClassName(classDescQualifiedName,
183                         currentAstSuperClassName)) {
184                     classDesc.registerNestedSubclass();
185                 }
186             }
187         }
188     }
189 
190     /**
191      * Get qualified class name from given class Ast.
192      * @param classAst class to get qualified class name
193      * @return qualified class name of a class
194      */
195     private String getQualifiedClassName(DetailAST classAst) {
196         final String className = classAst.findFirstToken(TokenTypes.IDENT).getText();
197         String outerClassQualifiedName = null;
198         if (!classes.isEmpty()) {
199             outerClassQualifiedName = classes.peek().getQualifiedName();
200         }
201         return getQualifiedClassName(packageName, outerClassQualifiedName, className);
202     }
203 
204     /**
205      * Calculate qualified class name(package + class name) laying inside given
206      * outer class.
207      * @param packageName package name, empty string on default package
208      * @param outerClassQualifiedName qualified name(package + class) of outer class,
209      *                           null if doesn't exist
210      * @param className class name
211      * @return qualified class name(package + class name)
212      */
213     private static String getQualifiedClassName(String packageName, String outerClassQualifiedName,
214                                                 String className) {
215         final String qualifiedClassName;
216 
217         if (outerClassQualifiedName == null) {
218             if (packageName.isEmpty()) {
219                 qualifiedClassName = className;
220             }
221             else {
222                 qualifiedClassName = packageName + PACKAGE_SEPARATOR + className;
223             }
224         }
225         else {
226             qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className;
227         }
228         return qualifiedClassName;
229     }
230 
231     /**
232      * Get super class name of given class.
233      * @param classAst class
234      * @return super class name or null if super class is not specified
235      */
236     private static String getSuperClassName(DetailAST classAst) {
237         String superClassName = null;
238         final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE);
239         if (classExtend != null) {
240             superClassName = extractQualifiedName(classExtend);
241         }
242         return superClassName;
243     }
244 
245     /**
246      * Checks if given super class name in extend clause match super class qualified name.
247      * @param superClassQualifiedName super class qualified name (with package)
248      * @param superClassInExtendClause name in extend clause
249      * @return true if given super class name in extend clause match super class qualified name,
250      *         false otherwise
251      */
252     private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName,
253                                                                String superClassInExtendClause) {
254         String superClassNormalizedName = superClassQualifiedName;
255         if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) {
256             superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName);
257         }
258         return superClassNormalizedName.equals(superClassInExtendClause);
259     }
260 
261     /**
262      * Get class name from qualified name.
263      * @param qualifiedName qualified class name
264      * @return class name
265      */
266     private static String getClassNameFromQualifiedName(String qualifiedName) {
267         return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1);
268     }
269 
270     /** Maintains information about class' ctors. */
271     private static final class ClassDesc {
272         /** Qualified class name(with package). */
273         private final String qualifiedName;
274 
275         /** Is class declared as final. */
276         private final boolean declaredAsFinal;
277 
278         /** Is class declared as abstract. */
279         private final boolean declaredAsAbstract;
280 
281         /** Does class have non-private ctors. */
282         private boolean withNonPrivateCtor;
283 
284         /** Does class have private ctors. */
285         private boolean withPrivateCtor;
286 
287         /** Does class have nested subclass. */
288         private boolean withNestedSubclass;
289 
290         /**
291          *  Create a new ClassDesc instance.
292          *  @param qualifiedName qualified class name(with package)
293          *  @param declaredAsFinal indicates if the
294          *         class declared as final
295          *  @param declaredAsAbstract indicates if the
296          *         class declared as abstract
297          */
298         ClassDesc(String qualifiedName, boolean declaredAsFinal, boolean declaredAsAbstract) {
299             this.qualifiedName = qualifiedName;
300             this.declaredAsFinal = declaredAsFinal;
301             this.declaredAsAbstract = declaredAsAbstract;
302         }
303 
304         /**
305          * Get qualified class name.
306          * @return qualified class name
307          */
308         private String getQualifiedName() {
309             return qualifiedName;
310         }
311 
312         /** Adds private ctor. */
313         private void registerPrivateCtor() {
314             withPrivateCtor = true;
315         }
316 
317         /** Adds non-private ctor. */
318         private void registerNonPrivateCtor() {
319             withNonPrivateCtor = true;
320         }
321 
322         /** Adds nested subclass. */
323         private void registerNestedSubclass() {
324             withNestedSubclass = true;
325         }
326 
327         /**
328          *  Does class have private ctors.
329          *  @return true if class has private ctors
330          */
331         private boolean isWithPrivateCtor() {
332             return withPrivateCtor;
333         }
334 
335         /**
336          *  Does class have non-private ctors.
337          *  @return true if class has non-private ctors
338          */
339         private boolean isWithNonPrivateCtor() {
340             return withNonPrivateCtor;
341         }
342 
343         /**
344          * Does class have nested subclass.
345          * @return true if class has nested subclass
346          */
347         private boolean isWithNestedSubclass() {
348             return withNestedSubclass;
349         }
350 
351         /**
352          *  Is class declared as final.
353          *  @return true if class is declared as final
354          */
355         private boolean isDeclaredAsFinal() {
356             return declaredAsFinal;
357         }
358 
359         /**
360          *  Is class declared as abstract.
361          *  @return true if class is declared as final
362          */
363         private boolean isDeclaredAsAbstract() {
364             return declaredAsAbstract;
365         }
366     }
367 }