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.Collections; 024import java.util.Deque; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Set; 033 034import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 035import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 036import com.puppycrawl.tools.checkstyle.api.DetailAST; 037import com.puppycrawl.tools.checkstyle.api.TokenTypes; 038import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 039import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 040import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 041 042/** 043 * <div> 044 * Checks that a local variable is declared and/or assigned, but not used. 045 * Supports 046 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.30"> 047 * pattern variables</a>. 048 * Doesn't check 049 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.12.3"> 050 * array components</a> as array components are classified as different kind of variables by 051 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/index.html">JLS</a>. 052 * </div> 053 * 054 * @since 9.3 055 */ 056@FileStatefulCheck 057public class UnusedLocalVariableCheck extends AbstractCheck { 058 059 /** 060 * A key is pointing to the warning message text in "messages.properties" file. 061 */ 062 public static final String MSG_UNUSED_LOCAL_VARIABLE = "unused.local.var"; 063 064 /** 065 * A key is pointing to the warning message text in "messages.properties" file. 066 */ 067 public static final String MSG_UNUSED_NAMED_LOCAL_VARIABLE = "unused.named.local.var"; 068 069 /** 070 * An array of increment and decrement tokens. 071 */ 072 private static final int[] INCREMENT_AND_DECREMENT_TOKENS = { 073 TokenTypes.POST_INC, 074 TokenTypes.POST_DEC, 075 TokenTypes.INC, 076 TokenTypes.DEC, 077 }; 078 079 /** 080 * An array of scope tokens. 081 */ 082 private static final int[] SCOPES = { 083 TokenTypes.SLIST, 084 TokenTypes.LITERAL_FOR, 085 TokenTypes.OBJBLOCK, 086 }; 087 088 /** 089 * An array of unacceptable children of ast of type {@link TokenTypes#DOT}. 090 */ 091 private static final int[] UNACCEPTABLE_CHILD_OF_DOT = { 092 TokenTypes.DOT, 093 TokenTypes.METHOD_CALL, 094 TokenTypes.LITERAL_NEW, 095 TokenTypes.LITERAL_SUPER, 096 TokenTypes.LITERAL_CLASS, 097 TokenTypes.LITERAL_THIS, 098 }; 099 100 /** 101 * An array of unacceptable parent of ast of type {@link TokenTypes#IDENT}. 102 */ 103 private static final int[] UNACCEPTABLE_PARENT_OF_IDENT = { 104 TokenTypes.VARIABLE_DEF, 105 TokenTypes.DOT, 106 TokenTypes.LITERAL_NEW, 107 TokenTypes.PATTERN_VARIABLE_DEF, 108 TokenTypes.METHOD_CALL, 109 TokenTypes.TYPE, 110 }; 111 112 /** 113 * An array of blocks in which local anon inner classes can exist. 114 */ 115 private static final int[] ANONYMOUS_CLASS_PARENT_TOKENS = { 116 TokenTypes.METHOD_DEF, 117 TokenTypes.CTOR_DEF, 118 TokenTypes.STATIC_INIT, 119 TokenTypes.INSTANCE_INIT, 120 TokenTypes.COMPACT_CTOR_DEF, 121 }; 122 123 /** 124 * An array of token types that indicate a variable is being used within 125 * an expression involving increment or decrement operators, or within a switch statement. 126 * When a token of one of these types is the parent of an expression, it indicates that the 127 * variable associated with the increment or decrement operation is being used. 128 * Ex:- TokenTypes.LITERAL_SWITCH: Indicates a switch statement. Variables used within the 129 * switch expression are considered to be used 130 */ 131 private static final int[] INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES = { 132 TokenTypes.ELIST, 133 TokenTypes.INDEX_OP, 134 TokenTypes.ASSIGN, 135 TokenTypes.LITERAL_SWITCH, 136 }; 137 138 /** Package separator. */ 139 private static final String PACKAGE_SEPARATOR = "."; 140 141 /** 142 * Symbol used to represent unnamed variables in Java pattern matching. 143 */ 144 private static final String UNNAMED_VAR = "_"; 145 146 /** 147 * Constant for JDK 22 version number. 148 */ 149 private static final int JDK_22 = 22; 150 151 /** 152 * Keeps tracks of the variables declared in file. 153 */ 154 private final Deque<VariableDesc> variables = new ArrayDeque<>(); 155 156 /** 157 * Keeps track of all the type declarations present in the file. 158 * Pops the type out of the stack while leaving the type in visitor pattern. 159 */ 160 private final Deque<TypeDeclDesc> typeDeclarations = new ArrayDeque<>(); 161 162 /** 163 * Maps type declaration ast to their respective TypeDeclDesc objects. 164 */ 165 private final Map<DetailAST, TypeDeclDesc> typeDeclAstToTypeDeclDesc = new LinkedHashMap<>(); 166 167 /** 168 * Maps local anonymous inner class to the TypeDeclDesc object containing it. 169 */ 170 private final Map<DetailAST, TypeDeclDesc> anonInnerAstToTypeDeclDesc = new HashMap<>(); 171 172 /** 173 * Set of tokens of type {@link UnusedLocalVariableCheck#ANONYMOUS_CLASS_PARENT_TOKENS} 174 * and {@link TokenTypes#LAMBDA} in some cases. 175 */ 176 private final Set<DetailAST> anonInnerClassHolders = new HashSet<>(); 177 178 /** 179 * Allow variables named with a single underscore 180 * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html"> 181 * unnamed variables</a> in Java 21+). 182 */ 183 private boolean allowUnnamedVariables = true; 184 185 /** 186 * Set the JDK version that you are using. 187 * Old JDK version numbering is supported (e.g. 1.8 for Java 8) 188 * as well as just the major JDK version alone (e.g. 8) is supported. 189 * This property only considers features from officially released 190 * Java versions as supported. Features introduced in preview releases 191 * are not considered supported until they are included in a non-preview release. 192 * Before JDK 22, named pattern variables in switch labels and instanceof 193 * record Destructuring cannot be replaced with {@code _}, so violations 194 * on them are suppressed when jdkVersion is set below 22. 195 */ 196 private int jdkVersion = JDK_22; 197 198 /** 199 * Name of the package. 200 */ 201 private String packageName; 202 203 /** 204 * Depth at which a type declaration is nested, 0 for top level type declarations. 205 */ 206 private int depth; 207 208 /** 209 * Creates a new {@code UnusedLocalVariableCheck} instance. 210 */ 211 public UnusedLocalVariableCheck() { 212 // no code by default 213 } 214 215 /** 216 * Setter to allow variables named with a single underscore 217 * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html"> 218 * unnamed variables</a> in Java 21+). 219 * 220 * @param allowUnnamedVariables true or false. 221 * @since 10.18.0 222 */ 223 public void setAllowUnnamedVariables(boolean allowUnnamedVariables) { 224 this.allowUnnamedVariables = allowUnnamedVariables; 225 } 226 227 /** 228 * Setter to set the JDK version that you are using. 229 * Old JDK version numbering is supported (e.g. 1.8 for Java 8) 230 * as well as just the major JDK version alone (e.g. 8) is supported. 231 * This property only considers features from officially released 232 * Java versions as supported. Features introduced in preview releases 233 * are not considered supported until they are included in a non-preview release. 234 * Before JDK 22, named pattern variables in switch labels and instanceof 235 * record Destructuring cannot be replaced with {@code _}, so violations 236 * on them are suppressed when jdkVersion is set below 22. 237 * 238 * @param jdkVersion the Java version. 239 * @since 13.7.0 240 */ 241 public void setJdkVersion(String jdkVersion) { 242 final String singleVersionNumber; 243 if (jdkVersion.startsWith("1.")) { 244 singleVersionNumber = jdkVersion.substring(2); 245 } 246 else { 247 singleVersionNumber = jdkVersion; 248 } 249 this.jdkVersion = Integer.parseInt(singleVersionNumber); 250 } 251 252 @Override 253 public int[] getDefaultTokens() { 254 return new int[] { 255 TokenTypes.DOT, 256 TokenTypes.VARIABLE_DEF, 257 TokenTypes.IDENT, 258 TokenTypes.SLIST, 259 TokenTypes.LITERAL_FOR, 260 TokenTypes.OBJBLOCK, 261 TokenTypes.CLASS_DEF, 262 TokenTypes.INTERFACE_DEF, 263 TokenTypes.ANNOTATION_DEF, 264 TokenTypes.PACKAGE_DEF, 265 TokenTypes.LITERAL_NEW, 266 TokenTypes.METHOD_DEF, 267 TokenTypes.CTOR_DEF, 268 TokenTypes.STATIC_INIT, 269 TokenTypes.INSTANCE_INIT, 270 TokenTypes.COMPILATION_UNIT, 271 TokenTypes.LAMBDA, 272 TokenTypes.ENUM_DEF, 273 TokenTypes.RECORD_DEF, 274 TokenTypes.COMPACT_CTOR_DEF, 275 TokenTypes.PATTERN_VARIABLE_DEF, 276 }; 277 } 278 279 @Override 280 public int[] getAcceptableTokens() { 281 return getDefaultTokens(); 282 } 283 284 @Override 285 public int[] getRequiredTokens() { 286 return getDefaultTokens(); 287 } 288 289 @Override 290 public void beginTree(DetailAST root) { 291 variables.clear(); 292 typeDeclarations.clear(); 293 typeDeclAstToTypeDeclDesc.clear(); 294 anonInnerAstToTypeDeclDesc.clear(); 295 anonInnerClassHolders.clear(); 296 packageName = null; 297 depth = 0; 298 } 299 300 @Override 301 public void visitToken(DetailAST ast) { 302 final int type = ast.getType(); 303 if (type == TokenTypes.DOT) { 304 visitDotToken(ast, variables); 305 } 306 else if (type == TokenTypes.VARIABLE_DEF && !skipUnnamedVariables(ast)) { 307 visitVariableDefToken(ast); 308 } 309 else if (type == TokenTypes.PATTERN_VARIABLE_DEF 310 && !skipUnnamedPatternVariables(ast)) { 311 addPatternVariable(ast, variables); 312 } 313 else if (type == TokenTypes.IDENT) { 314 visitIdentToken(ast, variables); 315 } 316 else if (isInsideLocalAnonInnerClass(ast)) { 317 visitLocalAnonInnerClass(ast); 318 } 319 else if (isNonLocalTypeDeclaration(ast)) { 320 visitNonLocalTypeDeclarationToken(ast); 321 } 322 else if (type == TokenTypes.PACKAGE_DEF) { 323 packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling()); 324 } 325 } 326 327 @Override 328 public void leaveToken(DetailAST ast) { 329 if (TokenUtil.isOfType(ast, SCOPES)) { 330 logViolations(ast, variables); 331 } 332 else if (ast.getType() == TokenTypes.COMPILATION_UNIT) { 333 leaveCompilationUnit(); 334 } 335 else if (isNonLocalTypeDeclaration(ast)) { 336 depth--; 337 typeDeclarations.pop(); 338 } 339 } 340 341 /** 342 * Visit ast of type {@link TokenTypes#DOT}. 343 * 344 * @param dotAst dotAst 345 * @param variablesStack stack of all the relevant variables in the scope 346 */ 347 private static void visitDotToken(DetailAST dotAst, Deque<VariableDesc> variablesStack) { 348 if (dotAst.getParent().getType() != TokenTypes.LITERAL_NEW 349 && shouldCheckIdentTokenNestedUnderDot(dotAst)) { 350 final DetailAST identifier = dotAst.findFirstToken(TokenTypes.IDENT); 351 if (identifier != null) { 352 checkIdentifierAst(identifier, variablesStack); 353 } 354 } 355 } 356 357 /** 358 * Visit ast of type {@link TokenTypes#VARIABLE_DEF}. 359 * 360 * @param varDefAst varDefAst 361 */ 362 private void visitVariableDefToken(DetailAST varDefAst) { 363 addLocalVariables(varDefAst, variables); 364 addInstanceOrClassVar(varDefAst); 365 } 366 367 /** 368 * Visit ast of type {@link TokenTypes#IDENT}. 369 * 370 * @param identAst identAst 371 * @param variablesStack stack of all the relevant variables in the scope 372 */ 373 private static void visitIdentToken(DetailAST identAst, Deque<VariableDesc> variablesStack) { 374 final DetailAST parent = identAst.getParent(); 375 final boolean isMethodReferenceMethodName = parent.getType() == TokenTypes.METHOD_REF 376 && parent.getFirstChild() != identAst; 377 final boolean isConstructorReference = parent.getType() == TokenTypes.METHOD_REF 378 && parent.getLastChild().getType() == TokenTypes.LITERAL_NEW; 379 final boolean isNestedClassInitialization = 380 TokenUtil.isOfType(identAst.getNextSibling(), TokenTypes.LITERAL_NEW) 381 && parent.getType() == TokenTypes.DOT; 382 383 if (isNestedClassInitialization || !isMethodReferenceMethodName 384 && !isConstructorReference 385 && !TokenUtil.isOfType(parent, UNACCEPTABLE_PARENT_OF_IDENT)) { 386 checkIdentifierAst(identAst, variablesStack); 387 } 388 } 389 390 /** 391 * Visit the non-local type declaration token. 392 * 393 * @param typeDeclAst type declaration ast 394 */ 395 private void visitNonLocalTypeDeclarationToken(DetailAST typeDeclAst) { 396 final String qualifiedName = getQualifiedTypeDeclarationName(typeDeclAst); 397 final TypeDeclDesc currTypeDecl = new TypeDeclDesc(qualifiedName, depth, typeDeclAst); 398 depth++; 399 typeDeclarations.push(currTypeDecl); 400 typeDeclAstToTypeDeclDesc.put(typeDeclAst, currTypeDecl); 401 } 402 403 /** 404 * Visit the local anon inner class. 405 * 406 * @param literalNewAst literalNewAst 407 */ 408 private void visitLocalAnonInnerClass(DetailAST literalNewAst) { 409 anonInnerAstToTypeDeclDesc.put(literalNewAst, typeDeclarations.peek()); 410 anonInnerClassHolders.add(getBlockContainingLocalAnonInnerClass(literalNewAst)); 411 } 412 413 /** 414 * Check for skip current {@link TokenTypes#VARIABLE_DEF} 415 * due to <b>allowUnnamedVariable</b> option. 416 * 417 * @param varDefAst varDefAst variable to check 418 * @return true if the current variable should be skipped. 419 */ 420 private boolean skipUnnamedVariables(DetailAST varDefAst) { 421 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT); 422 return allowUnnamedVariables && UNNAMED_VAR.equals(ident.getText()); 423 } 424 425 /** 426 * Checks whether the specified current pattern variable is an unnamed pattern variable. 427 * 428 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF} 429 * @return true if the current pattern variable should be skipped. 430 */ 431 private static boolean skipUnnamedPatternVariables(DetailAST patternVarDefAst) { 432 final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT); 433 return UNNAMED_VAR.equals(ident.getText()); 434 } 435 436 /** 437 * Add a pattern variable to the {@code variablesStack} stack. 438 * 439 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF} 440 * @param variablesStack stack of all the relevant variables in the scope 441 */ 442 private static void addPatternVariable(DetailAST patternVarDefAst, 443 Deque<VariableDesc> variablesStack) { 444 final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT); 445 final DetailAST scope = findScopeOfPatternVariable(patternVarDefAst); 446 final VariableDesc desc = new VariableDesc(ident.getText(), ident, scope); 447 if (isForcedNamePatternVariable(patternVarDefAst)) { 448 desc.registerAsNamedPatternVar(); 449 } 450 variablesStack.push(desc); 451 } 452 453 /** 454 * Checks whether the pattern variable is declared in a switch labels and instanceof. 455 * 456 * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF} 457 * @return true if the pattern variable is in a forced-name context 458 */ 459 private static boolean isForcedNamePatternVariable(DetailAST patternVarDefAst) { 460 return patternVarDefAst.getParent().getType() != TokenTypes.LITERAL_INSTANCEOF; 461 } 462 463 /** 464 * Find the scope of a pattern variable. 465 * 466 * @param patternVarDefAst ast of type. 467 * @return the outermost enclosing {@link TokenTypes#SLIST}, or {@code null} if none. 468 */ 469 private static DetailAST findScopeOfPatternVariable(DetailAST patternVarDefAst) { 470 final Deque<DetailAST> slistAncestors = new ArrayDeque<>(); 471 for (DetailAST current = patternVarDefAst; 472 current != null; 473 current = current.getParent()) { 474 if (current.getType() == TokenTypes.SLIST) { 475 slistAncestors.push(current); 476 } 477 } 478 return slistAncestors.peekLast(); 479 } 480 481 /** 482 * Whether ast node of type {@link TokenTypes#LITERAL_NEW} is a part of a local 483 * anonymous inner class. 484 * 485 * @param literalNewAst ast node of type {@code TokenTypes#LITERAL_NEW} 486 * @return true if variableDefAst is an instance variable in local anonymous inner class 487 */ 488 private static boolean isInsideLocalAnonInnerClass(DetailAST literalNewAst) { 489 boolean result = false; 490 final DetailAST lastChild = literalNewAst.getLastChild(); 491 if (lastChild != null && lastChild.getType() == TokenTypes.OBJBLOCK) { 492 DetailAST currentAst = literalNewAst; 493 while (currentAst != null 494 && !TokenUtil.isTypeDeclaration(currentAst.getType())) { 495 if (currentAst.getType() == TokenTypes.SLIST) { 496 result = true; 497 break; 498 } 499 currentAst = currentAst.getParent(); 500 } 501 } 502 return result; 503 } 504 505 /** 506 * Traverse {@code variablesStack} stack and log the violations. 507 * 508 * @param scopeAst ast node of type {@link UnusedLocalVariableCheck#SCOPES} 509 * @param variablesStack stack of all the relevant variables in the scope 510 */ 511 private void logViolations(DetailAST scopeAst, Deque<VariableDesc> variablesStack) { 512 final Iterator<VariableDesc> iterator = variablesStack.iterator(); 513 while (iterator.hasNext()) { 514 final VariableDesc variableDesc = iterator.next(); 515 if (variableDesc.getScope() == scopeAst) { 516 iterator.remove(); 517 if (!variableDesc.isUsed() 518 && !variableDesc.isInstVarOrClassVar() 519 && !(jdkVersion < JDK_22 520 && variableDesc.isNamedPatternVar())) { 521 final DetailAST typeAst = variableDesc.getTypeAst(); 522 if (allowUnnamedVariables) { 523 log(typeAst, MSG_UNUSED_NAMED_LOCAL_VARIABLE, variableDesc.getName()); 524 } 525 else { 526 log(typeAst, MSG_UNUSED_LOCAL_VARIABLE, variableDesc.getName()); 527 } 528 } 529 } 530 } 531 } 532 533 /** 534 * We process all the blocks containing local anonymous inner classes 535 * separately after processing all the other nodes. This is being done 536 * due to the fact the instance variables of local anon inner classes can 537 * cast a shadow on local variables. 538 */ 539 private void leaveCompilationUnit() { 540 anonInnerClassHolders.forEach(holder -> { 541 iterateOverBlockContainingLocalAnonInnerClass(holder, new ArrayDeque<>()); 542 }); 543 } 544 545 /** 546 * Whether a type declaration is non-local. Annotated interfaces are always non-local. 547 * 548 * @param typeDeclAst type declaration ast 549 * @return true if type declaration is non-local 550 */ 551 private static boolean isNonLocalTypeDeclaration(DetailAST typeDeclAst) { 552 return TokenUtil.isTypeDeclaration(typeDeclAst.getType()) 553 && typeDeclAst.getParent().getType() != TokenTypes.SLIST; 554 } 555 556 /** 557 * Get the block containing local anon inner class. 558 * 559 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 560 * @return the block containing local anon inner class 561 */ 562 private static DetailAST getBlockContainingLocalAnonInnerClass(DetailAST literalNewAst) { 563 DetailAST currentAst = literalNewAst; 564 DetailAST result = null; 565 DetailAST topMostLambdaAst = null; 566 boolean continueSearch = true; 567 while (continueSearch) { 568 continueSearch = false; 569 while (currentAst != null 570 && !TokenUtil.isOfType(currentAst, ANONYMOUS_CLASS_PARENT_TOKENS)) { 571 if (currentAst.getType() == TokenTypes.LAMBDA) { 572 topMostLambdaAst = currentAst; 573 currentAst = currentAst.getParent(); 574 continueSearch = true; 575 break; 576 } 577 currentAst = currentAst.getParent(); 578 result = currentAst; 579 } 580 } 581 582 if (currentAst == null) { 583 result = topMostLambdaAst; 584 } 585 return result; 586 } 587 588 /** 589 * Add local variables to the {@code variablesStack} stack. 590 * Also adds the instance variables defined in a local anonymous inner class. 591 * 592 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF} 593 * @param variablesStack stack of all the relevant variables in the scope 594 */ 595 private static void addLocalVariables(DetailAST varDefAst, Deque<VariableDesc> variablesStack) { 596 final DetailAST parentAst = varDefAst.getParent(); 597 final DetailAST grandParent = parentAst.getParent(); 598 599 if (grandParent != null) { 600 final boolean isInstanceVarInInnerClass = 601 grandParent.getType() == TokenTypes.LITERAL_NEW 602 || grandParent.getType() == TokenTypes.CLASS_DEF; 603 if (isInstanceVarInInnerClass 604 || parentAst.getType() != TokenTypes.OBJBLOCK) { 605 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT); 606 final VariableDesc desc = new VariableDesc(ident.getText(), 607 varDefAst.findFirstToken(TokenTypes.TYPE), findScopeOfVariable(varDefAst)); 608 if (isInstanceVarInInnerClass) { 609 desc.registerAsInstOrClassVar(); 610 } 611 variablesStack.push(desc); 612 } 613 } 614 } 615 616 /** 617 * Add instance variables and class variables to the 618 * {@link TypeDeclDesc#instanceAndClassVarStack}. 619 * 620 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF} 621 */ 622 private void addInstanceOrClassVar(DetailAST varDefAst) { 623 final DetailAST parentAst = varDefAst.getParent(); 624 final DetailAST grandParentAst = parentAst.getParent(); 625 if (grandParentAst != null 626 && isNonLocalTypeDeclaration(grandParentAst) 627 && !isPrivateInstanceVariable(varDefAst)) { 628 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT); 629 final VariableDesc desc = new VariableDesc(ident.getText()); 630 typeDeclAstToTypeDeclDesc.get(grandParentAst).addInstOrClassVar(desc); 631 } 632 } 633 634 /** 635 * Whether instance variable or class variable have private access modifier. 636 * 637 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF} 638 * @return true if instance variable or class variable have private access modifier 639 */ 640 private static boolean isPrivateInstanceVariable(DetailAST varDefAst) { 641 final AccessModifierOption varAccessModifier = 642 CheckUtil.getAccessModifierFromModifiersToken(varDefAst); 643 return varAccessModifier == AccessModifierOption.PRIVATE; 644 } 645 646 /** 647 * Get the {@link TypeDeclDesc} of the super class of anonymous inner class. 648 * 649 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 650 * @return {@code TypeDeclDesc} of the super class of anonymous inner class 651 */ 652 private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) { 653 TypeDeclDesc obtainedClass = null; 654 final String shortNameOfClass = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst); 655 if (packageName != null && shortNameOfClass.startsWith(packageName)) { 656 final Optional<TypeDeclDesc> classWithCompletePackageName = 657 typeDeclAstToTypeDeclDesc.values() 658 .stream() 659 .filter(typeDeclDesc -> { 660 return typeDeclDesc.getQualifiedName().equals(shortNameOfClass); 661 }) 662 .findFirst(); 663 if (classWithCompletePackageName.isPresent()) { 664 obtainedClass = classWithCompletePackageName.orElseThrow(); 665 } 666 } 667 else { 668 final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass); 669 if (!typeDeclWithSameName.isEmpty()) { 670 obtainedClass = getClosestMatchingTypeDeclaration( 671 anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(), 672 typeDeclWithSameName); 673 } 674 } 675 return obtainedClass; 676 } 677 678 /** 679 * Add non-private instance and class variables of the super class of the anonymous class 680 * to the variables stack. 681 * 682 * @param obtainedClass super class of the anon inner class 683 * @param variablesStack stack of all the relevant variables in the scope 684 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 685 */ 686 private void modifyVariablesStack(TypeDeclDesc obtainedClass, 687 Deque<VariableDesc> variablesStack, 688 DetailAST literalNewAst) { 689 if (obtainedClass != null) { 690 final Deque<VariableDesc> instAndClassVarDeque = typeDeclAstToTypeDeclDesc 691 .get(obtainedClass.getTypeDeclAst()) 692 .getUpdatedCopyOfVarStack(literalNewAst); 693 instAndClassVarDeque.forEach(variablesStack::push); 694 } 695 } 696 697 /** 698 * Checks if there is a type declaration with same name as the super class. 699 * 700 * @param superClassName name of the super class 701 * @return list if there is another type declaration with same name. 702 */ 703 private List<TypeDeclDesc> typeDeclWithSameName(String superClassName) { 704 return typeDeclAstToTypeDeclDesc.values().stream() 705 .filter(typeDeclDesc -> { 706 return hasSameNameAsSuperClass(superClassName, typeDeclDesc); 707 }) 708 .toList(); 709 } 710 711 /** 712 * Whether the qualified name of {@code typeDeclDesc} matches the super class name. 713 * 714 * @param superClassName name of the super class 715 * @param typeDeclDesc type declaration description 716 * @return {@code true} if the qualified name of {@code typeDeclDesc} 717 * matches the super class name 718 */ 719 private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc typeDeclDesc) { 720 final boolean result; 721 if (packageName == null && typeDeclDesc.getDepth() == 0) { 722 result = typeDeclDesc.getQualifiedName().equals(superClassName); 723 } 724 else { 725 result = typeDeclDesc.getQualifiedName() 726 .endsWith(PACKAGE_SEPARATOR + superClassName); 727 } 728 return result; 729 } 730 731 /** 732 * For all type declarations with the same name as the superclass, gets the nearest type 733 * declaration. 734 * 735 * @param outerTypeDeclName outer type declaration of anonymous inner class 736 * @param typeDeclWithSameName typeDeclarations which have the same name as the super class 737 * @return the nearest class 738 */ 739 private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName, 740 List<TypeDeclDesc> typeDeclWithSameName) { 741 return Collections.min(typeDeclWithSameName, (first, second) -> { 742 return calculateTypeDeclarationDistance(outerTypeDeclName, first, second); 743 }); 744 } 745 746 /** 747 * Get the difference between type declaration name matching count. If the 748 * difference between them is zero, then their depth is compared to obtain the result. 749 * 750 * @param outerTypeName outer type declaration of anonymous inner class 751 * @param firstType first input type declaration 752 * @param secondType second input type declaration 753 * @return difference between type declaration name matching count 754 */ 755 private static int calculateTypeDeclarationDistance(String outerTypeName, 756 TypeDeclDesc firstType, 757 TypeDeclDesc secondType) { 758 final int firstMatchCount = 759 countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName()); 760 final int secondMatchCount = 761 countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName()); 762 final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount); 763 764 final int distance; 765 if (matchDistance == 0) { 766 distance = Integer.compare(firstType.getDepth(), secondType.getDepth()); 767 } 768 else { 769 distance = matchDistance; 770 } 771 772 return distance; 773 } 774 775 /** 776 * Calculates the type declaration matching count for the superclass of an anonymous inner 777 * class. 778 * 779 * <p> 780 * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is 781 * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by 782 * comparing the characters at each position, and updating the count whenever a '.' 783 * is encountered. 784 * This is necessary because pattern class can include anonymous inner classes, unlike regular 785 * inheritance where nested classes cannot be extended. 786 * </p> 787 * 788 * @param pattern type declaration to match against 789 * @param candidate type declaration to be matched 790 * @return the type declaration matching count 791 */ 792 private static int countMatchingQualifierChars(String pattern, 793 String candidate) { 794 final int typeDeclarationToBeMatchedLength = candidate.length(); 795 final int minLength = Math 796 .min(typeDeclarationToBeMatchedLength, pattern.length()); 797 final boolean shouldCountBeUpdatedAtLastCharacter = 798 typeDeclarationToBeMatchedLength > minLength 799 && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0); 800 801 int result = 0; 802 for (int idx = 0; 803 idx < minLength 804 && pattern.charAt(idx) == candidate.charAt(idx); 805 idx++) { 806 807 if (shouldCountBeUpdatedAtLastCharacter 808 || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) { 809 result = idx; 810 } 811 } 812 return result; 813 } 814 815 /** 816 * Get qualified type declaration name from type ast. 817 * 818 * @param typeDeclAst type declaration ast 819 * @return qualified name of type declaration 820 */ 821 private String getQualifiedTypeDeclarationName(DetailAST typeDeclAst) { 822 final String className = typeDeclAst.findFirstToken(TokenTypes.IDENT).getText(); 823 String outerClassQualifiedName = null; 824 if (!typeDeclarations.isEmpty()) { 825 outerClassQualifiedName = typeDeclarations.peek().getQualifiedName(); 826 } 827 return CheckUtil 828 .getQualifiedTypeDeclarationName(packageName, outerClassQualifiedName, className); 829 } 830 831 /** 832 * Iterate over all the ast nodes present under {@code ast}. 833 * 834 * @param ast ast 835 * @param variablesStack stack of all the relevant variables in the scope 836 */ 837 private void iterateOverBlockContainingLocalAnonInnerClass( 838 DetailAST ast, Deque<VariableDesc> variablesStack) { 839 DetailAST currNode = ast; 840 while (currNode != null) { 841 customVisitToken(currNode, variablesStack); 842 DetailAST toVisit = currNode.getFirstChild(); 843 while (currNode != ast && toVisit == null) { 844 customLeaveToken(currNode, variablesStack); 845 toVisit = currNode.getNextSibling(); 846 currNode = currNode.getParent(); 847 } 848 currNode = toVisit; 849 } 850 } 851 852 /** 853 * Visit all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once again. 854 * 855 * @param ast ast 856 * @param variablesStack stack of all the relevant variables in the scope 857 */ 858 private void customVisitToken(DetailAST ast, Deque<VariableDesc> variablesStack) { 859 final int type = ast.getType(); 860 switch (type) { 861 case TokenTypes.DOT -> visitDotToken(ast, variablesStack); 862 863 case TokenTypes.VARIABLE_DEF -> { 864 if (!skipUnnamedVariables(ast)) { 865 addLocalVariables(ast, variablesStack); 866 } 867 } 868 869 case TokenTypes.IDENT -> visitIdentToken(ast, variablesStack); 870 871 case TokenTypes.LITERAL_NEW -> { 872 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 873 final TypeDeclDesc obtainedClass = getSuperClassOfAnonInnerClass(ast); 874 modifyVariablesStack(obtainedClass, variablesStack, ast); 875 } 876 } 877 878 default -> { 879 // No action needed for other token types 880 } 881 } 882 } 883 884 /** 885 * Leave all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once again. 886 * 887 * @param ast ast 888 * @param variablesStack stack of all the relevant variables in the scope 889 */ 890 private void customLeaveToken(DetailAST ast, Deque<VariableDesc> variablesStack) { 891 logViolations(ast, variablesStack); 892 } 893 894 /** 895 * Whether to check identifier token nested under dotAst. 896 * 897 * @param dotAst dotAst 898 * @return true if ident nested under dotAst should be checked 899 */ 900 private static boolean shouldCheckIdentTokenNestedUnderDot(DetailAST dotAst) { 901 902 return TokenUtil.findFirstTokenByPredicate(dotAst, 903 childAst -> { 904 return TokenUtil.isOfType(childAst, 905 UNACCEPTABLE_CHILD_OF_DOT); 906 }) 907 .isEmpty(); 908 } 909 910 /** 911 * Checks the identifier ast. 912 * 913 * @param identAst ast of type {@link TokenTypes#IDENT} 914 * @param variablesStack stack of all the relevant variables in the scope 915 */ 916 private static void checkIdentifierAst(DetailAST identAst, Deque<VariableDesc> variablesStack) { 917 for (VariableDesc variableDesc : variablesStack) { 918 if (identAst.getText().equals(variableDesc.getName()) 919 && !isLeftHandSideValue(identAst)) { 920 variableDesc.registerAsUsed(); 921 break; 922 } 923 } 924 } 925 926 /** 927 * Find the scope of variable. 928 * 929 * @param variableDef ast of type {@link TokenTypes#VARIABLE_DEF} 930 * @return scope of variableDef 931 */ 932 private static DetailAST findScopeOfVariable(DetailAST variableDef) { 933 final DetailAST result; 934 final DetailAST parentAst = variableDef.getParent(); 935 if (TokenUtil.isOfType(parentAst, TokenTypes.SLIST, TokenTypes.OBJBLOCK)) { 936 result = parentAst; 937 } 938 else { 939 result = parentAst.getParent(); 940 } 941 return result; 942 } 943 944 /** 945 * Checks whether the ast of type {@link TokenTypes#IDENT} is 946 * used as left-hand side value. An identifier is being used as a left-hand side 947 * value if it is used as the left operand of an assignment or as an 948 * operand of a stand-alone increment or decrement. 949 * 950 * @param identAst ast of type {@code TokenTypes#IDENT} 951 * @return true if identAst is used as a left-hand side value 952 */ 953 private static boolean isLeftHandSideValue(DetailAST identAst) { 954 final DetailAST parent = identAst.getParent(); 955 return isStandAloneIncrementOrDecrement(identAst) 956 || parent.getType() == TokenTypes.ASSIGN 957 && identAst != parent.getLastChild(); 958 } 959 960 /** 961 * Checks whether the ast of type {@link TokenTypes#IDENT} is used as 962 * an operand of a stand-alone increment or decrement. 963 * 964 * @param identAst ast of type {@code TokenTypes#IDENT} 965 * @return true if identAst is used as an operand of stand-alone increment or decrement 966 */ 967 private static boolean isStandAloneIncrementOrDecrement(DetailAST identAst) { 968 final DetailAST parent = identAst.getParent(); 969 final DetailAST grandParent = parent.getParent(); 970 return TokenUtil.isOfType(parent, INCREMENT_AND_DECREMENT_TOKENS) 971 && TokenUtil.isOfType(grandParent, TokenTypes.EXPR) 972 && !isIncrementOrDecrementVariableUsed(grandParent); 973 } 974 975 /** 976 * A variable with increment or decrement operator is considered used if it 977 * is used as an argument or as an array index or for assigning value to a variable. 978 * 979 * @param exprAst ast of type {@link TokenTypes#EXPR} 980 * @return true if variable nested in exprAst is used 981 */ 982 private static boolean isIncrementOrDecrementVariableUsed(DetailAST exprAst) { 983 return TokenUtil.isOfType(exprAst.getParent(), INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES) 984 && exprAst.getParent().getParent().getType() != TokenTypes.FOR_ITERATOR; 985 } 986 987 /** 988 * Maintains information about the variable. 989 */ 990 private static final class VariableDesc { 991 992 /** 993 * The name of the variable. 994 */ 995 private final String name; 996 997 /** 998 * Ast of type {@link TokenTypes#TYPE}. 999 */ 1000 private final DetailAST typeAst; 1001 1002 /** 1003 * The scope of variable is determined by the ast of type 1004 * {@link TokenTypes#SLIST} or {@link TokenTypes#LITERAL_FOR} 1005 * or {@link TokenTypes#OBJBLOCK} which is enclosing the variable. 1006 */ 1007 private final DetailAST scope; 1008 1009 /** 1010 * Is an instance variable or a class variable. 1011 */ 1012 private boolean instVarOrClassVar; 1013 1014 /** 1015 * Is a named pattern variable declared in a switch label. 1016 */ 1017 private boolean namedPatternVar; 1018 1019 /** 1020 * Is the variable used. 1021 */ 1022 private boolean used; 1023 1024 /** 1025 * Create a new VariableDesc instance. 1026 * 1027 * @param name name of the variable 1028 */ 1029 private VariableDesc(String name) { 1030 this(name, null, null); 1031 } 1032 1033 /** 1034 * Create a new VariableDesc instance. 1035 * 1036 * @param name name of the variable 1037 * @param scope ast of type {@link TokenTypes#SLIST} or 1038 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 1039 * which is enclosing the variable 1040 */ 1041 private VariableDesc(String name, DetailAST scope) { 1042 this(name, null, scope); 1043 } 1044 1045 /** 1046 * Create a new VariableDesc instance. 1047 * 1048 * @param name name of the variable 1049 * @param typeAst ast of type {@link TokenTypes#TYPE} 1050 * @param scope ast of type {@link TokenTypes#SLIST} or 1051 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 1052 * which is enclosing the variable 1053 */ 1054 private VariableDesc(String name, DetailAST typeAst, DetailAST scope) { 1055 this.name = name; 1056 this.typeAst = typeAst; 1057 this.scope = scope; 1058 } 1059 1060 /** 1061 * Get the name of variable. 1062 * 1063 * @return name of variable 1064 */ 1065 /* package */ String getName() { 1066 return name; 1067 } 1068 1069 /** 1070 * Get the associated ast node of type {@link TokenTypes#TYPE}. 1071 * 1072 * @return the associated ast node of type {@code TokenTypes#TYPE} 1073 */ 1074 /* package */ DetailAST getTypeAst() { 1075 return typeAst; 1076 } 1077 1078 /** 1079 * Get ast of type {@link TokenTypes#SLIST} 1080 * or {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 1081 * which is enclosing the variable i.e. its scope. 1082 * 1083 * @return the scope associated with the variable 1084 */ 1085 /* package */ DetailAST getScope() { 1086 return scope; 1087 } 1088 1089 /** 1090 * Register the variable as used. 1091 */ 1092 /* package */ void registerAsUsed() { 1093 used = true; 1094 } 1095 1096 /** 1097 * Register the variable as an instance variable or class variable. 1098 */ 1099 /* package */ void registerAsInstOrClassVar() { 1100 instVarOrClassVar = true; 1101 } 1102 1103 /** 1104 * Register the variable as a forced-name pattern variable declared 1105 * in a switch label or instanceof record Destructuring. 1106 */ 1107 /* package */ void registerAsNamedPatternVar() { 1108 namedPatternVar = true; 1109 } 1110 1111 /** 1112 * Is the variable used or not. 1113 * 1114 * @return true if variable is used 1115 */ 1116 /* package */ boolean isUsed() { 1117 return used; 1118 } 1119 1120 /** 1121 * Is an instance variable or a class variable. 1122 * 1123 * @return true if is an instance variable or a class variable 1124 */ 1125 /* package */ boolean isInstVarOrClassVar() { 1126 return instVarOrClassVar; 1127 } 1128 1129 /** 1130 * Is a forced-name pattern variable from a switch label or instanceof record Destructuring. 1131 * 1132 * @return true if this variable was declared in a context where 1133 * pre-JDK 22 forces a name to be given even when unused 1134 */ 1135 /* package */ boolean isNamedPatternVar() { 1136 return namedPatternVar; 1137 } 1138 1139 } 1140 1141 /** 1142 * Maintains information about the type declaration. 1143 * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF} 1144 * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF} 1145 * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration. 1146 */ 1147 private static final class TypeDeclDesc { 1148 1149 /** 1150 * Complete type declaration name with package name and outer type declaration name. 1151 */ 1152 private final String qualifiedName; 1153 1154 /** 1155 * Depth of nesting of type declaration. 1156 */ 1157 private final int depth; 1158 1159 /** 1160 * Type declaration ast node. 1161 */ 1162 private final DetailAST typeDeclAst; 1163 1164 /** 1165 * A stack of type declaration's instance and static variables. 1166 */ 1167 private final Deque<VariableDesc> instanceAndClassVarStack; 1168 1169 /** 1170 * Create a new TypeDeclDesc instance. 1171 * 1172 * @param qualifiedName qualified name 1173 * @param depth depth of nesting 1174 * @param typeDeclAst type declaration ast node 1175 */ 1176 private TypeDeclDesc(String qualifiedName, int depth, 1177 DetailAST typeDeclAst) { 1178 this.qualifiedName = qualifiedName; 1179 this.depth = depth; 1180 this.typeDeclAst = typeDeclAst; 1181 instanceAndClassVarStack = new ArrayDeque<>(); 1182 } 1183 1184 /** 1185 * Get the complete type declaration name i.e. type declaration name with package name 1186 * and outer type declaration name. 1187 * 1188 * @return qualified class name 1189 */ 1190 /* package */ String getQualifiedName() { 1191 return qualifiedName; 1192 } 1193 1194 /** 1195 * Get the depth of type declaration. 1196 * 1197 * @return the depth of nesting of type declaration 1198 */ 1199 /* package */ int getDepth() { 1200 return depth; 1201 } 1202 1203 /** 1204 * Get the type declaration ast node. 1205 * 1206 * @return ast node of the type declaration 1207 */ 1208 /* package */ DetailAST getTypeDeclAst() { 1209 return typeDeclAst; 1210 } 1211 1212 /** 1213 * Get the copy of variables in instanceAndClassVar stack with updated scope. 1214 * 1215 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 1216 * @return copy of variables in instanceAndClassVar stack with updated scope. 1217 */ 1218 /* package */ Deque<VariableDesc> getUpdatedCopyOfVarStack(DetailAST literalNewAst) { 1219 final DetailAST updatedScope = literalNewAst; 1220 final Deque<VariableDesc> instAndClassVarDeque = new ArrayDeque<>(); 1221 instanceAndClassVarStack.forEach(instVar -> { 1222 final VariableDesc variableDesc = new VariableDesc(instVar.getName(), 1223 updatedScope); 1224 variableDesc.registerAsInstOrClassVar(); 1225 instAndClassVarDeque.push(variableDesc); 1226 }); 1227 return instAndClassVarDeque; 1228 } 1229 1230 /** 1231 * Add an instance variable or class variable to the stack. 1232 * 1233 * @param variableDesc variable to be added 1234 */ 1235 /* package */ void addInstOrClassVar(VariableDesc variableDesc) { 1236 instanceAndClassVarStack.push(variableDesc); 1237 } 1238 1239 } 1240 1241}