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   *
20   * <p>An example of how to configure the check is:</p>
21   * <pre>
22   * &lt;module name="ProhibitNonFinalClassesCheck"/&gt;
23   * </pre>
24   *
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;
38  
39      /**
40       * Full qualified name of the package.
41       */
42      private String pack;
43  
44      /**
45       * Default constructor.
46       */
47      public ProhibitNonFinalClassesCheck() {
48          this.classes = new ArrayDeque<>();
49      }
50  
51      @Override
52      public int[] getDefaultTokens() {
53          return this.getRequiredTokens();
54      }
55  
56      @Override
57      public int[] getAcceptableTokens() {
58          return this.getRequiredTokens();
59      }
60  
61      @Override
62      public int[] getRequiredTokens() {
63          return new int[] {TokenTypes.CLASS_DEF};
64      }
65  
66      @Override
67      public void beginTree(final DetailAST root) {
68          this.classes = new ArrayDeque<>();
69          this.pack = "";
70      }
71  
72      @Override
73      public void visitToken(final DetailAST ast) {
74          final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
75          if (ast.getType() == TokenTypes.CLASS_DEF) {
76              this.classes.push(
77                  new ClassDesc(
78                      this.qualifiedClassName(ast),
79                      modifiers.findFirstToken(TokenTypes.FINAL) != null,
80                      modifiers.findFirstToken(TokenTypes.ABSTRACT) != null
81                  )
82              );
83          }
84      }
85  
86      @Override
87      public void leaveToken(final DetailAST ast) {
88          if (ast.getType() == TokenTypes.CLASS_DEF) {
89              final ClassDesc desc = this.classes.pop();
90              if (!desc.isDeclaredAsAbstract()
91                  && !desc.isAsfinal()
92                  && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
93                  log(
94                      ast.getLineNo(),
95                      "Classes should be final",
96                      ProhibitNonFinalClassesCheck.getClassNameFromQualifiedName(
97                          desc.getQualified()
98                      )
99                  );
100             }
101         }
102     }
103 
104     private String qualifiedClassName(final DetailAST classast) {
105         String outer = null;
106         if (!this.classes.isEmpty()) {
107             outer = this.classes.peek().getQualified();
108         }
109         return ProhibitNonFinalClassesCheck.getQualifiedClassName(
110             this.pack,
111             outer,
112             classast.findFirstToken(TokenTypes.IDENT).getText()
113         );
114     }
115 
116     private static String getQualifiedClassName(
117         final String pack,
118         final String outer,
119         final String name) {
120         final String qualified;
121         if (outer == null) {
122             if (pack.isEmpty()) {
123                 qualified = name;
124             } else {
125                 qualified =
126                     new UncheckedText(
127                         new Joined(
128                             ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
129                             pack,
130                             name
131                         )
132                     ).asString();
133             }
134         } else {
135             qualified =
136                 new UncheckedText(
137                     new Joined(
138                         ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
139                         outer,
140                         name
141                     )
142                 ).asString();
143         }
144         return qualified;
145     }
146 
147     private static String getClassNameFromQualifiedName(
148         final String qualified
149     ) {
150         return qualified.substring(
151             qualified.lastIndexOf(
152                 ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR
153             ) + 1
154         );
155     }
156 }