View Javadoc
1   /*
2    * Copyright (c) 2011-2024 Qulice.com
3    *
4    * All rights reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions
8    * are met: 1) Redistributions of source code must retain the above
9    * copyright notice, this list of conditions and the following
10   * disclaimer. 2) Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following
12   * disclaimer in the documentation and/or other materials provided
13   * with the distribution. 3) Neither the name of the Qulice.com nor
14   * the names of its contributors may be used to endorse or promote
15   * products derived from this software without specific prior written
16   * permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
20   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29   * OF THE POSSIBILITY OF SUCH DAMAGE.
30   */
31  package com.qulice.checkstyle;
32  
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.ScopeUtil;
37  import java.util.ArrayDeque;
38  import java.util.Deque;
39  import org.cactoos.text.Joined;
40  import org.cactoos.text.UncheckedText;
41  
42  /**
43   * Checks that classes are declared as final. Doesn't check for classes nested
44   *  in interfaces or annotations, as they are always {@code final} there.
45   * <p>
46   * An example of how to configure the check is:
47   * </p>
48   * <pre>
49   * &lt;module name="ProhibitNonFinalClassesCheck"/&gt;
50   * </pre>
51   *
52   * @since 0.19
53   */
54  public final class ProhibitNonFinalClassesCheck extends AbstractCheck {
55  
56      /**
57       * Character separate package names in qualified name of java class.
58       */
59      private static final String PACKAGE_SEPARATOR = ".";
60  
61      /**
62      * Keeps ClassDesc objects for stack of declared classes.
63      */
64      private Deque<ClassDesc> classes = new ArrayDeque<>();
65  
66      /**
67      * Full qualified name of the package.
68      */
69      private String pack;
70  
71      @Override
72      public int[] getDefaultTokens() {
73          return this.getRequiredTokens();
74      }
75  
76      @Override
77      public int[] getAcceptableTokens() {
78          return this.getRequiredTokens();
79      }
80  
81      @Override
82      public int[] getRequiredTokens() {
83          return new int[] {TokenTypes.CLASS_DEF};
84      }
85  
86      @Override
87      public void beginTree(final DetailAST root) {
88          this.classes = new ArrayDeque<>();
89          this.pack = "";
90      }
91  
92      @Override
93      public void visitToken(final DetailAST ast) {
94          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
95          if (ast.getType() == TokenTypes.CLASS_DEF) {
96              final boolean isfinal =
97                  modifiers.findFirstToken(TokenTypes.FINAL) != null;
98              final boolean isabstract =
99                  modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
100             final String qualified = this.qualifiedClassName(ast);
101             this.classes.push(
102                 new ClassDesc(qualified, isfinal, isabstract)
103             );
104         }
105     }
106 
107     @Override
108     public void leaveToken(final DetailAST ast) {
109         if (ast.getType() == TokenTypes.CLASS_DEF) {
110             final ClassDesc desc = this.classes.pop();
111             if (!desc.isDeclaredAsAbstract()
112                 && !desc.isAsfinal()
113                 && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
114                 final String qualified = desc.getQualified();
115                 final String name =
116                     ProhibitNonFinalClassesCheck.getClassNameFromQualifiedName(
117                         qualified
118                     );
119                 log(ast.getLineNo(), "Classes should be final", name);
120             }
121         }
122     }
123 
124     /**
125      * Get qualified class name from given class Ast.
126      * @param classast Class to get qualified class name
127      * @return Qualified class name of a class
128     */
129     private String qualifiedClassName(final DetailAST classast) {
130         final String name = classast.findFirstToken(
131             TokenTypes.IDENT
132         ).getText();
133         String outer = null;
134         if (!this.classes.isEmpty()) {
135             outer = this.classes.peek().getQualified();
136         }
137         return ProhibitNonFinalClassesCheck.getQualifiedClassName(
138             this.pack,
139             outer,
140             name
141         );
142     }
143 
144     /**
145      * Calculate qualified class name(package + class name) laying inside given
146      * outer class.
147      * @param pack Package name, empty string on default package
148      * @param outer Qualified name(package + class) of outer
149      *  class, null if doesn't exist
150      * @param name Class name
151      * @return Qualified class name(package + class name)
152     */
153     private static String getQualifiedClassName(
154         final String pack,
155         final String outer,
156         final String name) {
157         final String qualified;
158         if (outer == null) {
159             if (pack.isEmpty()) {
160                 qualified = name;
161             } else {
162                 qualified =
163                     new UncheckedText(
164                         new Joined(
165                             ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
166                             pack,
167                             name
168                         )
169                     ).asString();
170             }
171         } else {
172             qualified =
173                 new UncheckedText(
174                     new Joined(
175                         ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
176                         outer,
177                         name
178                     )
179                 ).asString();
180         }
181         return qualified;
182     }
183 
184     /**
185      * Get class name from qualified name.
186      * @param qualified Qualified class name
187      * @return Class Name
188      */
189     private static String getClassNameFromQualifiedName(
190         final String qualified
191     ) {
192         return qualified.substring(
193             qualified.lastIndexOf(
194                 ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR
195             ) + 1
196         );
197     }
198 
199     /**
200      * Maintains information about class' ctors.
201      *
202      * @since 0.1
203      */
204     private static final class ClassDesc {
205 
206         /**
207          * Qualified class name(with package).
208         */
209         private final String qualified;
210 
211         /**
212          * Is class declared as final.
213         */
214         private final boolean asfinal;
215 
216         /**
217          * Is class declared as abstract.
218         */
219         private final boolean asabstract;
220 
221         /**
222          * Create a new ClassDesc instance.
223          *
224          * @param qualified Qualified class name(with package)
225          * @param asfinal Indicates if the class declared as final
226          * @param asabstract Indicates if the class declared as
227          *  abstract
228          */
229         ClassDesc(final String qualified, final boolean asfinal,
230             final boolean asabstract
231         ) {
232             this.qualified = qualified;
233             this.asfinal = asfinal;
234             this.asabstract = asabstract;
235         }
236 
237         /**
238          * Get qualified class name.
239          * @return Qualified class name
240          */
241         private String getQualified() {
242             return this.qualified;
243         }
244 
245         /**
246          * Is class declared as final.
247          * @return True if class is declared as final
248          */
249         private boolean isAsfinal() {
250             return this.asfinal;
251         }
252 
253         /**
254          * Is class declared as abstract.
255          * @return True if class is declared as final
256          */
257         private boolean isDeclaredAsAbstract() {
258             return this.asabstract;
259         }
260     }
261 }