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