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.modifier; 021 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Map; 026import java.util.Set; 027 028import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.FullIdent; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 034import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 035import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 036import com.puppycrawl.tools.checkstyle.utils.NullUtil; 037 038/** 039 * <div> 040 * Checks that elements annotated with specified annotations 041 * have only allowed visibility modifiers. 042 * </div> 043 * 044 * <p> 045 * This check enforces consistency between annotation presence and 046 * declared visibility. If a configured annotation is found on a target 047 * element, its visibility modifier must match one of the allowed values. 048 * </p> 049 * 050 * @since 13.9.0 051 */ 052@FileStatefulCheck 053public class AnnotatedDeclarationVisibilityCheck extends AbstractCheck { 054 055 /** 056 * Message key for violation. 057 */ 058 public static final String MSG_KEY = "annotated.visibility.modifier"; 059 060 /** 061 * Dot. 062 */ 063 private static final char DOT = '.'; 064 065 /** 066 * Configured annotation canonical names. 067 */ 068 private final Set<String> annotations = new HashSet<>( 069 Set.of("com.google.common.annotations.VisibleForTesting")); 070 071 /** 072 * Set of non star imports. 073 */ 074 private final Map<String, String> importedAnnotations = new HashMap<>(); 075 076 /** 077 * Set of star imports. 078 */ 079 private final Set<String> starImports = new HashSet<>(); 080 081 /** 082 * Allowed visibility values. 083 */ 084 private AccessModifierOption[] visibility = { 085 AccessModifierOption.PROTECTED, 086 AccessModifierOption.PACKAGE, 087 }; 088 089 /** 090 * Current package. 091 */ 092 private String currentPackage; 093 094 /** 095 * Creates a new {@code AnnotatedDeclarationVisibilityCheck} instance. 096 */ 097 public AnnotatedDeclarationVisibilityCheck() { 098 // no code by default 099 } 100 101 /** 102 * Setter for annotation canonical names. 103 * 104 * @param values comma-separated fully qualified annotation names 105 * @since 13.9.0 106 */ 107 public void setAnnotations(String... values) { 108 annotations.clear(); 109 annotations.addAll(Arrays.asList(values)); 110 } 111 112 /** 113 * Setter for allowed visibility modifiers. 114 * Allowed values: 115 * public, protected, package, private. 116 * 117 * @param values allowed visibility values 118 * @since 13.9.0 119 */ 120 public void setVisibility(AccessModifierOption... values) { 121 visibility = Arrays.copyOf(values, values.length); 122 } 123 124 @Override 125 public void beginTree(DetailAST rootAST) { 126 currentPackage = ""; 127 importedAnnotations.clear(); 128 starImports.clear(); 129 } 130 131 @Override 132 public int[] getDefaultTokens() { 133 return getAcceptableTokens(); 134 } 135 136 @Override 137 public int[] getRequiredTokens() { 138 return new int[] { 139 // annotation name resolution 140 TokenTypes.PACKAGE_DEF, 141 TokenTypes.IMPORT, 142 }; 143 } 144 145 @Override 146 public int[] getAcceptableTokens() { 147 return new int[] { 148 // annotation name resolution 149 TokenTypes.PACKAGE_DEF, 150 TokenTypes.IMPORT, 151 // tokens that can have annotations 152 TokenTypes.CLASS_DEF, 153 TokenTypes.INTERFACE_DEF, 154 TokenTypes.ENUM_DEF, 155 TokenTypes.RECORD_DEF, 156 TokenTypes.METHOD_DEF, 157 TokenTypes.CTOR_DEF, 158 TokenTypes.VARIABLE_DEF, 159 TokenTypes.ANNOTATION_DEF, 160 }; 161 } 162 163 @Override 164 public void visitToken(DetailAST ast) { 165 switch (ast.getType()) { 166 case TokenTypes.PACKAGE_DEF -> handlePackage(ast); 167 case TokenTypes.IMPORT -> handleImport(ast); 168 default -> checkAnnotatedVisibility(ast); 169 } 170 } 171 172 /** 173 * Handles package declarations and stores the current package name. 174 * 175 * @param ast package definition node 176 */ 177 private void handlePackage(DetailAST ast) { 178 currentPackage = 179 FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling()).getText(); 180 } 181 182 /** 183 * Processes import statements and records imported annotations. 184 * 185 * @param ast import node 186 */ 187 private void handleImport(DetailAST ast) { 188 final String importText = FullIdent.createFullIdentBelow(ast).getText(); 189 if (importText.endsWith(".*")) { 190 starImports.add(importText.substring(0, importText.length() - 2)); 191 } 192 else { 193 final int lastDot = importText.lastIndexOf(DOT); 194 final String simple = importText.substring(lastDot + 1); 195 importedAnnotations.put(simple, importText); 196 } 197 } 198 199 /** 200 * Checks the visibility of annotated elements. 201 * 202 * @param ast AST node to inspect 203 */ 204 private void checkAnnotatedVisibility(DetailAST ast) { 205 if ((ast.getType() != TokenTypes.VARIABLE_DEF 206 || ast.getParent().getType() == TokenTypes.OBJBLOCK) 207 && hasConfiguredAnnotation(ast)) { 208 final AccessModifierOption accessModifierOption = 209 CheckUtil.getAccessModifierFromModifiersToken(ast); 210 if (!isAllowedVisibility(accessModifierOption)) { 211 log(ast, MSG_KEY, getVisibilityName(accessModifierOption)); 212 } 213 } 214 } 215 216 /** 217 * Determines whether the AST node contains a configured annotation. 218 * 219 * @param ast AST node to inspect 220 * @return true if the annotation is present 221 */ 222 private boolean hasConfiguredAnnotation(DetailAST ast) { 223 boolean result = false; 224 final DetailAST modifiers = 225 NullUtil.notNull(ast.findFirstToken(TokenTypes.MODIFIERS)); 226 DetailAST child = modifiers.getFirstChild(); 227 while (child != null) { 228 final String annotationText = 229 AnnotationUtil.getAnnotationFullIdent(child); 230 final String resolved = resolveAnnotation(annotationText); 231 if (annotations.contains(resolved)) { 232 result = true; 233 } 234 child = child.getNextSibling(); 235 } 236 return result; 237 } 238 239 /** 240 * Resolves the fully qualified name of an annotation. 241 * 242 * @param name annotation name 243 * @return resolved canonical name 244 */ 245 private String resolveAnnotation(String name) { 246 String result = name; 247 final String pkgCandidate = currentPackage + DOT + name; 248 final String importCandidate = importedAnnotations.get(name); 249 final String javaLangCandidate = "java.lang." + name; 250 if (annotations.contains(pkgCandidate)) { 251 result = pkgCandidate; 252 } 253 else if (importCandidate != null) { 254 result = importCandidate; 255 } 256 else if (annotations.contains(javaLangCandidate)) { 257 result = javaLangCandidate; 258 } 259 else { 260 for (String starImport : starImports) { 261 final String starCandidate = starImport + DOT + name; 262 if (annotations.contains(starCandidate)) { 263 result = starCandidate; 264 } 265 } 266 } 267 return result; 268 } 269 270 /** 271 * Checks whether the given visibility value is allowed. 272 * 273 * @param accessModifier visibility value 274 * @return true if given visibility value is allowed 275 */ 276 private boolean isAllowedVisibility(AccessModifierOption accessModifier) { 277 return Arrays.stream(visibility) 278 .anyMatch(modifier -> modifier == accessModifier); 279 } 280 281 /** 282 * Gets the visibility value to display in violation messages. 283 * 284 * @param accessModifier visibility value 285 * @return display name of the visibility value 286 */ 287 private static String getVisibilityName(AccessModifierOption accessModifier) { 288 final String result; 289 if (accessModifier == AccessModifierOption.PACKAGE) { 290 result = "package-private"; 291 } 292 else { 293 result = accessModifier.toString(); 294 } 295 return result; 296 } 297 298}