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 }