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