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