View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
11  import java.util.ArrayDeque;
12  import java.util.Deque;
13  import org.cactoos.text.Joined;
14  import org.cactoos.text.UncheckedText;
15  
16  /**
17   * Checks that classes are declared as final. Doesn't check for classes nested
18   *  in interfaces or annotations, as they are always {@code final} there.
19   * <p>
20   * An example of how to configure the check is:
21   * </p>
22   * <pre>
23   * &lt;module name="ProhibitNonFinalClassesCheck"/&gt;
24   * </pre>
25   * @since 0.19
26   */
27  public final class ProhibitNonFinalClassesCheck extends AbstractCheck {
28  
29      /**
30       * Character separate package names in qualified name of java class.
31       */
32      private static final String PACKAGE_SEPARATOR = ".";
33  
34      /**
35      * Keeps ClassDesc objects for stack of declared classes.
36      */
37      private Deque<ClassDesc> classes = new ArrayDeque<>();
38  
39      /**
40      * Full qualified name of the package.
41      */
42      private String pack;
43  
44      @Override
45      public int[] getDefaultTokens() {
46          return this.getRequiredTokens();
47      }
48  
49      @Override
50      public int[] getAcceptableTokens() {
51          return this.getRequiredTokens();
52      }
53  
54      @Override
55      public int[] getRequiredTokens() {
56          return new int[] {TokenTypes.CLASS_DEF};
57      }
58  
59      @Override
60      public void beginTree(final DetailAST root) {
61          this.classes = new ArrayDeque<>();
62          this.pack = "";
63      }
64  
65      @Override
66      public void visitToken(final DetailAST ast) {
67          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
68          if (ast.getType() == TokenTypes.CLASS_DEF) {
69              this.classes.push(
70                  new ProhibitNonFinalClassesCheck.ClassDesc(
71                      this.qualifiedClassName(ast),
72                      modifiers.findFirstToken(TokenTypes.FINAL) != null,
73                      modifiers.findFirstToken(TokenTypes.ABSTRACT) != null
74                  )
75              );
76          }
77      }
78  
79      @Override
80      public void leaveToken(final DetailAST ast) {
81          if (ast.getType() == TokenTypes.CLASS_DEF) {
82              final ClassDesc desc = this.classes.pop();
83              if (!desc.isDeclaredAsAbstract()
84                  && !desc.isAsfinal()
85                  && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
86                  log(
87                      ast.getLineNo(),
88                      "Classes should be final",
89                      ProhibitNonFinalClassesCheck.getClassNameFromQualifiedName(
90                          desc.getQualified()
91                      )
92                  );
93              }
94          }
95      }
96  
97      /**
98       * Get qualified class name from given class Ast.
99       * @param classast Class to get qualified class name
100      * @return Qualified class name of a class
101     */
102     private String qualifiedClassName(final DetailAST classast) {
103         String outer = null;
104         if (!this.classes.isEmpty()) {
105             outer = this.classes.peek().getQualified();
106         }
107         return ProhibitNonFinalClassesCheck.getQualifiedClassName(
108             this.pack,
109             outer,
110             classast.findFirstToken(TokenTypes.IDENT).getText()
111         );
112     }
113 
114     /**
115      * Calculate qualified class name(package + class name) laying inside given
116      * outer class.
117      * @param pack Package name, empty string on default package
118      * @param outer Qualified name(package + class) of outer
119      *  class, null if doesn't exist
120      * @param name Class name
121      * @return Qualified class name(package + class name)
122     */
123     private static String getQualifiedClassName(
124         final String pack,
125         final String outer,
126         final String name) {
127         final String qualified;
128         if (outer == null) {
129             if (pack.isEmpty()) {
130                 qualified = name;
131             } else {
132                 qualified =
133                     new UncheckedText(
134                         new Joined(
135                             ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
136                             pack,
137                             name
138                         )
139                     ).asString();
140             }
141         } else {
142             qualified =
143                 new UncheckedText(
144                     new Joined(
145                         ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
146                         outer,
147                         name
148                     )
149                 ).asString();
150         }
151         return qualified;
152     }
153 
154     /**
155      * Get class name from qualified name.
156      * @param qualified Qualified class name
157      * @return Class Name
158      */
159     private static String getClassNameFromQualifiedName(
160         final String qualified
161     ) {
162         return qualified.substring(
163             qualified.lastIndexOf(
164                 ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR
165             ) + 1
166         );
167     }
168 
169     /**
170      * Maintains information about class' ctors.
171      * @since 0.1
172      */
173     private static final class ClassDesc {
174 
175         /**
176          * Qualified class name(with package).
177         */
178         private final String qualified;
179 
180         /**
181          * Is class declared as final.
182         */
183         private final boolean asfinal;
184 
185         /**
186          * Is class declared as abstract.
187         */
188         private final boolean asabstract;
189 
190         /**
191          * Create a new ClassDesc instance.
192          * @param qualified Qualified class name(with package)
193          * @param asfinal Indicates if the class declared as final
194          * @param asabstract Indicates if the class declared as
195          *  abstract
196          */
197         ClassDesc(final String qualified, final boolean asfinal,
198             final boolean asabstract
199         ) {
200             this.qualified = qualified;
201             this.asfinal = asfinal;
202             this.asabstract = asabstract;
203         }
204 
205         /**
206          * Get qualified class name.
207          * @return Qualified class name
208          */
209         private String getQualified() {
210             return this.qualified;
211         }
212 
213         /**
214          * Is class declared as final.
215          * @return True if class is declared as final
216          */
217         private boolean isAsfinal() {
218             return this.asfinal;
219         }
220 
221         /**
222          * Is class declared as abstract.
223          * @return True if class is declared as final
224          */
225         private boolean isDeclaredAsAbstract() {
226             return this.asabstract;
227         }
228     }
229 }