001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import java.util.ArrayList; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Set; 026 027import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.TokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 032 033/** 034 * <div> 035 * Checks that a {@code permits} clause of a sealed class or interface is not 036 * unnecessary, i.e. that it is not possible to omit the clause and have the 037 * compiler infer the exact same set of permitted subtypes. 038 * </div> 039 * 040 * <p> 041 * See the <a href="https://docs.oracle.com/javase/specs/jls/se22/html/jls-13.html#jls-13.4.2"> 042 * Java Language Specification</a> for more information about sealed classes. 043 * </p> 044 * 045 * <p> 046 * This Check does not perform full type resolution. It determines whether a 047 * permitted type is local to the file by comparing simple names against every 048 * type declaration (class, interface, enum, or record) found 049 * anywhere in the compilation unit, including nested and sibling top-level 050 * types. As a result, a permitted type whose simple name coincidentally 051 * matches an unrelated local type declaration could, in theory, be 052 * misidentified as local. In practice this situation cannot occur in code 053 * that compiles, since the Java compiler would not be able to resolve such an 054 * ambiguous reference in the {@code permits} clause. 055 * </p> 056 * 057 * @since 14.2.0 058 */ 059@FileStatefulCheck 060public class UnnecessaryPermitsClauseCheck extends AbstractCheck { 061 062 /** 063 * A key is pointing to the warning message text in "messages.properties" 064 * file. 065 */ 066 public static final String MSG_KEY = "unnecessary.permits.clause"; 067 068 /** 069 * A set of simple names of every type declared in the current compilation 070 * unit, including nested types. 071 */ 072 private final Set<String> localTypeNames = new HashSet<>(); 073 074 /** 075 * A list of {@link TokenTypes#PERMITS_CLAUSE} nodes found in the current 076 * compilation unit. 077 */ 078 private final List<DetailAST> permitsClauseAstList = new ArrayList<>(); 079 080 /** 081 * Creates a new {@code UnnecessaryPermitsClauseCheck} instance. 082 */ 083 public UnnecessaryPermitsClauseCheck() { 084 // no code by default 085 } 086 087 @Override 088 public int[] getDefaultTokens() { 089 return getRequiredTokens(); 090 } 091 092 @Override 093 public int[] getAcceptableTokens() { 094 return getRequiredTokens(); 095 } 096 097 @Override 098 public int[] getRequiredTokens() { 099 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}