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.ArrayDeque; 023import java.util.ArrayList; 024import java.util.Deque; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.regex.Pattern; 031import java.util.stream.Collectors; 032 033import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 035import com.puppycrawl.tools.checkstyle.api.DetailAST; 036import com.puppycrawl.tools.checkstyle.api.FullIdent; 037import com.puppycrawl.tools.checkstyle.api.TokenTypes; 038import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 039import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 040 041/** 042 * <div> 043 * Check that a private field is declared, but never used. Fields with any other 044 * visibility (package-private, protected, or public), including those declared in 045 * an implicitly declared class (compact source file), are not checked. 046 * </div> 047 * 048 * @since 14.1.0 049 */ 050@FileStatefulCheck 051public class UnusedPrivateFieldCheck extends AbstractCheck { 052 053 /** 054 * A key is pointing to the warning message text in "messages.properties". 055 */ 056 public static final String MSG_PRIVATE_FIELD = "unused.private.field"; 057 058 /** 059 * Stack of private field maps, one per class nesting level. 060 */ 061 private final Deque<Map<String, DetailAST>> privateFields = new ArrayDeque<>(); 062 063 /** 064 * Stack of currently open enclosing type names. 065 */ 066 private final Deque<String> enclosingTypeNames = new ArrayDeque<>(); 067 068 /** 069 * Recorded field-name usage occurrences. 070 */ 071 private final List<FieldUsage> fieldUsages = new ArrayList<>(); 072 073 /** 074 * Global set of field names accessed. 075 */ 076 private final Set<String> globalUsedFields = new HashSet<>(); 077 078 /** 079 * Accumulated pending fields, reported at finishTree. 080 */ 081 private final List<PendingField> pendingFields = new ArrayList<>(); 082 083 /** 084 * Scope stack tracking local variable and parameter names per block. 085 */ 086 private final Deque<Map<String, String>> scopeStack = new ArrayDeque<>(); 087 088 /** 089 * Snapshots of scope stack saved when entering a nested class, 090 * restored when leaving it. 091 */ 092 private final Deque<Deque<Map<String, String>>> scopeStackSnapshots = new ArrayDeque<>(); 093 094 /** 095 * Recorded field-name accesses on a qualifier. 096 */ 097 private final List<TypedUsage> typedUsages = new ArrayList<>(); 098 099 /** 100 * Each class's private fields, keyed by simple type name, populated once 101 * a class's OBJBLOCK closes. Used to resolve {@link #typedUsages}. 102 */ 103 private final Map<String, Map<String, DetailAST>> privateFieldsByType = new HashMap<>(); 104 105 /** 106 * Specify annotations canonical names which ignore variables in consideration. 107 * A field is ignored either when the field itself carries a matching annotation, 108 * or when its enclosing class, interface, enum, or record carries one (e.g. a 109 * class-level Lombok {@code @Getter}). 110 */ 111 private Set<String> ignoreAnnotationCanonicalNames = new HashSet<>(Set.of("java.io.Serial")); 112 113 /** 114 * Specify a regular expression pattern for field names to ignore. 115 */ 116 private Pattern ignoredFieldPattern = Pattern.compile("serialVersionUID"); 117 118 /** 119 * Set of ignore annotations short names. 120 */ 121 private Set<String> ignoreAnnotationShortNames = new HashSet<>(); 122 123 /** 124 * Creates a new {@code UnusedPrivateFieldCheck} instance with default values. 125 * 126 */ 127 public UnusedPrivateFieldCheck() { 128 // default constructor 129 } 130 131 /** 132 * Setter to specify annotations canonical names which ignore variables in consideration. 133 * 134 * @param annotationNames array of ignore annotations canonical names. 135 * @since 14.1.0 136 */ 137 public void setIgnoreAnnotationCanonicalNames(String... annotationNames) { 138 ignoreAnnotationCanonicalNames = Set.of(annotationNames); 139 } 140 141 /** 142 * Setter to specify a regular expression pattern for field names to ignore, even 143 * if they otherwise satisfy this check's detection of an unused private field. 144 * Note this replaces the default value entirely — to keep {@code serialVersionUID} 145 * ignored alongside your own pattern, include it explicitly, e.g. 146 * {@code ^(serialVersionUID|LOG|LOGGER)$}. 147 * 148 * @param pattern regular expression pattern for field names to ignore. 149 * @since 14.1.0 150 */ 151 public void setIgnoredFieldPattern(Pattern pattern) { 152 ignoredFieldPattern = pattern; 153 } 154 155 @Override 156 public int[] getAcceptableTokens() { 157 return new int[] { 158 TokenTypes.IMPORT, 159 TokenTypes.OBJBLOCK, 160 TokenTypes.VARIABLE_DEF, 161 TokenTypes.PARAMETER_DEF, 162 TokenTypes.PARAMETERS, 163 TokenTypes.SLIST, 164 TokenTypes.IDENT, 165 TokenTypes.METHOD_DEF, 166 TokenTypes.CTOR_DEF, 167 TokenTypes.LAMBDA, 168 TokenTypes.LITERAL_FOR, 169 TokenTypes.LITERAL_CATCH, 170 }; 171 } 172 173 @Override 174 public int[] getDefaultTokens() { 175 return getAcceptableTokens(); 176 } 177 178 @Override 179 public int[] getRequiredTokens() { 180 return getAcceptableTokens(); 181 } 182 183 @Override 184 public void beginTree(DetailAST rootAST) { 185 privateFields.clear(); 186 enclosingTypeNames.clear(); 187 fieldUsages.clear(); 188 globalUsedFields.clear(); 189 pendingFields.clear(); 190 scopeStack.clear(); 191 scopeStackSnapshots.clear(); 192 typedUsages.clear(); 193 privateFieldsByType.clear(); 194 ignoreAnnotationShortNames = ignoreAnnotationCanonicalNames.stream() 195 .map(CommonUtil::baseClassName) 196 .collect(Collectors.toCollection(HashSet::new)); 197 } 198 199 @Override 200 public void visitToken(DetailAST ast) { 201 switch (ast.getType()) { 202 case TokenTypes.OBJBLOCK -> { 203 privateFields.push(new HashMap<>()); 204 scopeStackSnapshots.push(new ArrayDeque<>(scopeStack)); 205 scopeStack.clear(); 206 final DetailAST typeDef = ast.getParent(); 207 final DetailAST nameIdent = typeDef.findFirstToken(TokenTypes.IDENT); 208 if (nameIdent == null) { 209 enclosingTypeNames.push(""); 210 } 211 else { 212 enclosingTypeNames.push(nameIdent.getText()); 213 } 214 } 215 case TokenTypes.PARAMETERS, TokenTypes.SLIST, TokenTypes.LITERAL_FOR, 216 TokenTypes.LITERAL_CATCH -> scopeStack.push(new HashMap<>()); 217 case TokenTypes.PARAMETER_DEF -> { 218 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT); 219 if (ident != null) { 220 scopeStack.peek().put(ident.getText(), resolveDeclaredTypeName(ast)); 221 } 222 } 223 case TokenTypes.VARIABLE_DEF -> handleVariableDef(ast); 224 case TokenTypes.IDENT -> handleIdent(ast); 225 default -> { 226 // no action needed for other token types 227 } 228 } 229 } 230 231 @Override 232 public void leaveToken(DetailAST ast) { 233 switch (ast.getType()) { 234 case TokenTypes.OBJBLOCK -> { 235 final Map<String, DetailAST> classFields = privateFields.pop(); 236 privateFieldsByType.put(enclosingTypeNames.peek(), classFields); 237 for (final Map.Entry<String, DetailAST> entry : classFields.entrySet()) { 238 pendingFields.add(new PendingField(entry)); 239 } 240 final Deque<Map<String, String>> snapshot = scopeStackSnapshots.pop(); 241 snapshot.forEach(scopeStack::push); 242 enclosingTypeNames.pop(); 243 } 244 case TokenTypes.LAMBDA -> { 245 if (ast.findFirstToken(TokenTypes.PARAMETERS) != null) { 246 scopeStack.pop(); 247 } 248 } 249 case TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF, 250 TokenTypes.SLIST, TokenTypes.LITERAL_FOR, 251 TokenTypes.LITERAL_CATCH -> scopeStack.pop(); 252 default -> { 253 // no action needed for other token types 254 } 255 } 256 } 257 258 @Override 259 public void finishTree(final DetailAST rootAST) { 260 final Set<DetailAST> usedFieldIdents = new HashSet<>(); 261 fieldUsages.stream() 262 .map(UnusedPrivateFieldCheck::resolveUsage) 263 .forEach(usedFieldIdents::add); 264 for (final TypedUsage typedUsage : typedUsages) { 265 final Map<String, DetailAST> fields = privateFieldsByType.get(typedUsage.typeName()); 266 if (fields != null) { 267 final DetailAST ident = fields.get(typedUsage.fieldName()); 268 usedFieldIdents.add(ident); 269 270 } 271 } 272 for (final PendingField pending : pendingFields) { 273 final Map.Entry<String, DetailAST> entry = pending.entry(); 274 final DetailAST ident = entry.getValue(); 275 final String name = entry.getKey(); 276 if (!usedFieldIdents.contains(ident) && !globalUsedFields.contains(name)) { 277 log(ident, MSG_PRIVATE_FIELD, name); 278 } 279 } 280 } 281 282 /** 283 * Resolves a recorded usage to the exact field declaration it refers to, 284 * following the same shadowing rules Java itself applies. 285 * 286 * @param usage the recorded usage to resolve. 287 * @return the DetailAST of the field it resolves to, or null if none. 288 */ 289 private static DetailAST resolveUsage(FieldUsage usage) { 290 DetailAST result = null; 291 if (usage.qualifierTypeName() != null) { 292 final int index = usage.ancestorTypeNames().indexOf(usage.qualifierTypeName()); 293 if (index != -1) { 294 result = usage.ancestorFieldMaps().get(index).get(usage.name()); 295 } 296 } 297 else { 298 for (final Map<String, DetailAST> level : usage.ancestorFieldMaps()) { 299 result = level.get(usage.name()); 300 if (result != null) { 301 break; 302 } 303 } 304 } 305 return result; 306 } 307 308 /** 309 * Collects private field declarations. 310 * 311 * @param ast for this method. 312 */ 313 private void handleVariableDef(DetailAST ast) { 314 final DetailAST parent = ast.getParent(); 315 316 if (parent.getType() == TokenTypes.OBJBLOCK) { 317 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 318 final boolean isPrivateField = isPrivate(modifiers); 319 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT); 320 final boolean isIgnoredName = 321 ignoredFieldPattern.matcher(ident.getText()).matches(); 322 final boolean isIgnored = isIgnoredName || hasIgnoredAnnotation(ast); 323 if (isPrivateField && !isIgnored) { 324 privateFields.peek().put(ident.getText(), ident); 325 } 326 } 327 else if (!scopeStack.isEmpty()) { 328 final String localName = 329 ast.findFirstToken(TokenTypes.IDENT).getText(); 330 scopeStack.peek().put(localName, resolveDeclaredTypeName(ast)); 331 } 332 } 333 334 /** 335 * Checks whether a field should be ignored because either the field itself or its 336 * enclosing type (class, interface, enum, or record) carries an annotation present 337 * in {@link #ignoreAnnotationCanonicalNames} (matched by canonical or short name). 338 * 339 * @param variableDef the VARIABLE_DEF node. 340 * @return true if the field or its enclosing type has a matching ignore annotation. 341 */ 342 private boolean hasIgnoredAnnotation(final DetailAST variableDef) { 343 boolean result = isAnnotatedWithIgnoredAnnotation(variableDef); 344 if (!result) { 345 final DetailAST classDef = variableDef.getParent().getParent(); 346 result = isAnnotatedWithIgnoredAnnotation(classDef); 347 } 348 return result; 349 } 350 351 /** 352 * Checks whether the given AST node (a field or a type definition) carries an 353 * annotation present in {@link #ignoreAnnotationCanonicalNames}, matched either 354 * by canonical name or by short name. 355 * 356 * @param ast the VARIABLE_DEF, CLASS_DEF, INTERFACE_DEF, ENUM_DEF, RECORD_DEF, or 357 * ANNOTATION_DEF node to inspect. 358 * @return true if a matching ignore annotation is present directly on {@code ast}. 359 */ 360 private boolean isAnnotatedWithIgnoredAnnotation(final DetailAST ast) { 361 boolean result = false; 362 final DetailAST holder = AnnotationUtil.getAnnotationHolder(ast); 363 if (holder != null) { 364 DetailAST child = holder.getFirstChild(); 365 while (child != null) { 366 if (child.getType() == TokenTypes.ANNOTATION) { 367 final String name = 368 FullIdent.createFullIdent( 369 child.getFirstChild().getNextSibling()).getText(); 370 if (ignoreAnnotationCanonicalNames.contains(name) 371 || ignoreAnnotationShortNames.contains(name)) { 372 result = true; 373 break; 374 } 375 } 376 child = child.getNextSibling(); 377 } 378 } 379 return result; 380 } 381 382 /** 383 * Records field usage, respecting local variable and parameter shadowing. 384 * Resolution to a specific field declaration happens later, at 385 * {@link #finishTree}. 386 * 387 * @param ast for handleIdent 388 */ 389 private void handleIdent(DetailAST ast) { 390 final DetailAST parent = ast.getParent(); 391 if (!isDeclarationParent(parent)) { 392 final String name = ast.getText(); 393 final boolean shadowed = 394 scopeStack.stream().anyMatch(scope -> scope.containsKey(name)); 395 if (parent.getType() == TokenTypes.DOT) { 396 handleDotAccess(parent, name); 397 } 398 else if (!shadowed) { 399 recordUsage(name, null, false); 400 } 401 } 402 } 403 404 /** 405 * Classifies a dot-qualified reference: {@code this.field} and 406 * {@code ClassName.this.field} are resolved precisely; any other qualifier 407 * (an arbitrary object or type reference) cannot be resolved without type 408 * information, so it falls back to name-only matching via 409 * {@link #globalUsedFields}. 410 * 411 * @param dot the DOT node whose last child is the field IDENT. 412 * @param name the field name being accessed. 413 */ 414 private void handleDotAccess(DetailAST dot, String name) { 415 final DetailAST qualifier = dot.getFirstChild(); 416 if (qualifier.getType() == TokenTypes.LITERAL_THIS) { 417 recordUsage(name, null, true); 418 } 419 else if (qualifier.getType() == TokenTypes.DOT 420 && qualifier.getLastChild().getType() == TokenTypes.LITERAL_THIS) { 421 final String qualifiedName = 422 FullIdent.createFullIdent(qualifier.getFirstChild()).getText(); 423 final String simpleName = 424 qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1); 425 recordUsage(name, simpleName, false); 426 } 427 else if (findDeclaredType(qualifier.getText()) != null) { 428 typedUsages.add(new TypedUsage(findDeclaredType(qualifier.getText()), name)); 429 } 430 else { 431 globalUsedFields.add(name); 432 } 433 } 434 435 /** 436 * Looks up the declared simple type name of a local variable or parameter 437 * currently in scope. 438 * 439 * @param name the variable or parameter name. 440 * @return its declared simple type name, or null if the name is not a 441 * tracked local/parameter, or its type could not be determined. 442 */ 443 private String findDeclaredType(String name) { 444 String result = null; 445 for (final Map<String, String> scope : scopeStack) { 446 final String type = scope.get(name); 447 if (type != null) { 448 result = type; 449 break; 450 } 451 } 452 return result; 453 } 454 455 /** 456 * Extracts the declared simple type name of a VARIABLE_DEF or 457 * PARAMETER_DEF, if determinable. 458 * 459 * @param varOrParamDef the VARIABLE_DEF or PARAMETER_DEF node. 460 * @return the simple type name, or null if it could not be determined 461 * (e.g. primitive types, inferred/generic-only forms). 462 */ 463 private static String resolveDeclaredTypeName(DetailAST varOrParamDef) { 464 final DetailAST typeAst = varOrParamDef.findFirstToken(TokenTypes.TYPE); 465 String result = null; 466 final DetailAST identChild = typeAst.findFirstToken(TokenTypes.IDENT); 467 if (identChild != null) { 468 result = identChild.getText(); 469 } 470 return result; 471 } 472 473 /** 474 * Snapshots the currently open class chain (field maps and type names) so the 475 * usage can be resolved later, once every class in the file is fully populated. 476 * 477 * @param name the field name referenced. 478 * @param qualifierTypeName the enclosing type name named in a 479 * {@code ClassName.this.field} reference, or null. 480 * @param bareThisQualified true for a {@code this.field} reference. 481 */ 482 private void recordUsage(String name, String qualifierTypeName, boolean bareThisQualified) { 483 fieldUsages.add(new FieldUsage(name, 484 new ArrayList<>(privateFields), 485 new ArrayList<>(enclosingTypeNames), 486 qualifierTypeName, 487 bareThisQualified)); 488 } 489 490 /** 491 * Checks whether the given parent node is a declaration site whose IDENT child 492 * names the declared element itself (a variable, method, constructor, or type), 493 * rather than referencing some other field. 494 * 495 * @param parent the parent of the IDENT being inspected. 496 * @return true if the IDENT is a declaration name, not a usage. 497 */ 498 private static boolean isDeclarationParent(DetailAST parent) { 499 final int type = parent.getType(); 500 return type == TokenTypes.VARIABLE_DEF 501 || type == TokenTypes.METHOD_DEF 502 || type == TokenTypes.CLASS_DEF 503 || type == TokenTypes.INTERFACE_DEF 504 || type == TokenTypes.ENUM_DEF 505 || type == TokenTypes.RECORD_DEF 506 || type == TokenTypes.ANNOTATION_DEF; 507 } 508 509 /** 510 * Checks whether a field is private. 511 * 512 * @param modifiers for isPrivate method. 513 * @return modifiers of literal_private. 514 */ 515 private static boolean isPrivate(final DetailAST modifiers) { 516 return modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null; 517 } 518 519 /** 520 * Holds a private field entry, reported if never resolved to by any usage. 521 * 522 * @param entry The field name and its AST node. 523 */ 524 private record PendingField(Map.Entry<String, DetailAST> entry) { 525 } 526 527 /** 528 * A recorded field-name access on a qualifier whose declared type is 529 * known precisely. 530 * 531 * @param typeName the qualifier's declared simple type name. 532 * @param fieldName the field name accessed on it. 533 */ 534 private record TypedUsage(String typeName, String fieldName) { 535 } 536 537 /** 538 * A recorded, not-yet-resolved reference to a field name, with enough context 539 * to resolve it precisely once the whole file has been visited. 540 * 541 * @param name the field name referenced. 542 * @param ancestorFieldMaps the currently open field maps at the time of the 543 * reference, innermost first (index 0). 544 * @param ancestorTypeNames the currently open type names, parallel to 545 * {@code ancestorFieldMaps}. 546 * @param qualifierTypeName the type name in a {@code ClassName.this.field} 547 * reference, or null if not that form. 548 * @param bareThisQualified true for a {@code this.field} reference. 549 */ 550 private record FieldUsage(String name, List<Map<String, DetailAST>> ancestorFieldMaps, 551 List<String> ancestorTypeNames, String qualifierTypeName, 552 boolean bareThisQualified) { 553 } 554 555}