View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.coding;
21  
22  import java.util.ArrayList;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
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.TokenUtil;
32  
33  /**
34   * <div>
35   * Checks that a {@code permits} clause of a sealed class or interface is not
36   * unnecessary, i.e. that it is not possible to omit the clause and have the
37   * compiler infer the exact same set of permitted subtypes.
38   * </div>
39   *
40   * <p>
41   * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-13.html#jls-13.4.2">
42   * Java Language Specification</a> for more information about sealed classes.
43   * </p>
44   *
45   * <p>
46   * This Check does not perform full type resolution. It determines whether a
47   * permitted type is local to the file by comparing simple names against every
48   * type declaration (class, interface, enum, or record) found
49   * anywhere in the compilation unit, including nested and sibling top-level
50   * types. As a result, a permitted type whose simple name coincidentally
51   * matches an unrelated local type declaration could, in theory, be
52   * misidentified as local. In practice this situation cannot occur in code
53   * that compiles, since the Java compiler would not be able to resolve such an
54   * ambiguous reference in the {@code permits} clause.
55   * </p>
56   *
57   * @since 14.2.0
58   */
59  @FileStatefulCheck
60  public class UnnecessaryPermitsClauseCheck extends AbstractCheck {
61  
62      /**
63       * A key is pointing to the warning message text in "messages.properties"
64       * file.
65       */
66      public static final String MSG_KEY = "unnecessary.permits.clause";
67  
68      /**
69       * A set of simple names of every type declared in the current compilation
70       * unit, including nested types.
71       */
72      private final Set<String> localTypeNames = new HashSet<>();
73  
74      /**
75       * A list of {@link TokenTypes#PERMITS_CLAUSE} nodes found in the current
76       * compilation unit.
77       */
78      private final List<DetailAST> permitsClauseAstList = new ArrayList<>();
79  
80      /**
81       * Creates a new {@code UnnecessaryPermitsClauseCheck} instance.
82       */
83      public UnnecessaryPermitsClauseCheck() {
84          // no code by default
85      }
86  
87      @Override
88      public int[] getDefaultTokens() {
89          return getRequiredTokens();
90      }
91  
92      @Override
93      public int[] getAcceptableTokens() {
94          return getRequiredTokens();
95      }
96  
97      @Override
98      public int[] getRequiredTokens() {
99          return new int[] {
100             TokenTypes.CLASS_DEF,
101             TokenTypes.INTERFACE_DEF,
102             TokenTypes.ENUM_DEF,
103             TokenTypes.RECORD_DEF,
104             TokenTypes.PERMITS_CLAUSE,
105         };
106     }
107 
108     @Override
109     public void beginTree(DetailAST rootAST) {
110         localTypeNames.clear();
111         permitsClauseAstList.clear();
112     }
113 
114     @Override
115     public void visitToken(DetailAST ast) {
116         switch (ast.getType()) {
117             case TokenTypes.CLASS_DEF,
118                     TokenTypes.INTERFACE_DEF,
119                     TokenTypes.ENUM_DEF,
120                     TokenTypes.RECORD_DEF -> {
121                 final DetailAST nameAst = ast.findFirstToken(TokenTypes.IDENT);
122                 localTypeNames.add(nameAst.getText());
123             }
124             default -> permitsClauseAstList.add(ast);
125         }
126     }
127 
128     @Override
129     public void finishTree(DetailAST rootAST) {
130         for (DetailAST permitsClause : permitsClauseAstList) {
131             if (isUnnecessary(permitsClause)) {
132                 log(permitsClause, MSG_KEY);
133             }
134         }
135     }
136 
137     /**
138      * Determines whether every type named in the given {@code permits} clause
139      * is declared somewhere within the same compilation unit, and is
140      * therefore redundant.
141      *
142      * @param permitsClause the {@link TokenTypes#PERMITS_CLAUSE} node to inspect
143      * @return {@code true} if the clause is unnecessary
144      */
145     private boolean isUnnecessary(DetailAST permitsClause) {
146         boolean result = true;
147         DetailAST permittedType = permitsClause.getFirstChild();
148         while (permittedType != null) {
149             if (isTypeName(permittedType)) {
150                 final String simpleName = getSimpleName(permittedType);
151                 if (!localTypeNames.contains(simpleName)) {
152                     result = false;
153                     break;
154                 }
155             }
156             permittedType = permittedType.getNextSibling();
157         }
158         return result;
159     }
160 
161     /**
162      * Checks whether the given direct child of a {@code permits} clause
163      * represents a permitted type name, as opposed to a separating comma.
164      *
165      * @param ast the node to check
166      * @return {@code true} if the node is a (possibly qualified) type name
167      */
168     private static boolean isTypeName(DetailAST ast) {
169         return TokenUtil.isOfType(ast, TokenTypes.IDENT, TokenTypes.DOT);
170     }
171 
172     /**
173      * Extracts the simple (unqualified) name from a type name node, which is
174      * either a single {@link TokenTypes#IDENT} or a {@link TokenTypes#DOT}
175      * chain representing a qualified name.
176      *
177      * @param typeName the type name node
178      * @return the simple name of the type
179      */
180     private static String getSimpleName(DetailAST typeName) {
181         final String simpleName;
182         if (typeName.getType() == TokenTypes.DOT) {
183             simpleName = typeName.getLastChild().getText();
184         }
185         else {
186             simpleName = typeName.getText();
187         }
188         return simpleName;
189     }
190 
191 }