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 final boolean isInstanceVarInInnerClass = 537 grandParent.getType() == TokenTypes.LITERAL_NEW 538 || grandParent.getType() == TokenTypes.CLASS_DEF; 539 if (isInstanceVarInInnerClass 540 || parentAst.getType() != TokenTypes.OBJBLOCK) { 541 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT); 542 final VariableDesc desc = new VariableDesc(ident.getText(), 543 varDefAst.findFirstToken(TokenTypes.TYPE), findScopeOfVariable(varDefAst)); 544 if (isInstanceVarInInnerClass) { 545 desc.registerAsInstOrClassVar(); 546 } 547 variablesStack.push(desc); 548 } 549 } 550 551 /** 552 * Add instance variables and class variables to the 553 * {@link TypeDeclDesc#instanceAndClassVarStack}. 554 * 555 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF} 556 */ 557 private void addInstanceOrClassVar(DetailAST varDefAst) { 558 final DetailAST parentAst = varDefAst.getParent(); 559 if (isNonLocalTypeDeclaration(parentAst.getParent()) 560 && !isPrivateInstanceVariable(varDefAst)) { 561 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT); 562 final VariableDesc desc = new VariableDesc(ident.getText()); 563 typeDeclAstToTypeDeclDesc.get(parentAst.getParent()).addInstOrClassVar(desc); 564 } 565 } 566 567 /** 568 * Whether instance variable or class variable have private access modifier. 569 * 570 * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF} 571 * @return true if instance variable or class variable have private access modifier 572 */ 573 private static boolean isPrivateInstanceVariable(DetailAST varDefAst) { 574 final AccessModifierOption varAccessModifier = 575 CheckUtil.getAccessModifierFromModifiersToken(varDefAst); 576 return varAccessModifier == AccessModifierOption.PRIVATE; 577 } 578 579 /** 580 * Get the {@link TypeDeclDesc} of the super class of anonymous inner class. 581 * 582 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 583 * @return {@link TypeDeclDesc} of the super class of anonymous inner class 584 */ 585 private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) { 586 TypeDeclDesc obtainedClass = null; 587 final String shortNameOfClass = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst); 588 if (packageName != null && shortNameOfClass.startsWith(packageName)) { 589 final Optional<TypeDeclDesc> classWithCompletePackageName = 590 typeDeclAstToTypeDeclDesc.values() 591 .stream() 592 .filter(typeDeclDesc -> { 593 return typeDeclDesc.getQualifiedName().equals(shortNameOfClass); 594 }) 595 .findFirst(); 596 if (classWithCompletePackageName.isPresent()) { 597 obtainedClass = classWithCompletePackageName.orElseThrow(); 598 } 599 } 600 else { 601 final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass); 602 if (!typeDeclWithSameName.isEmpty()) { 603 obtainedClass = getClosestMatchingTypeDeclaration( 604 anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(), 605 typeDeclWithSameName); 606 } 607 } 608 return obtainedClass; 609 } 610 611 /** 612 * Add non-private instance and class variables of the super class of the anonymous class 613 * to the variables stack. 614 * 615 * @param obtainedClass super class of the anon inner class 616 * @param variablesStack stack of all the relevant variables in the scope 617 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 618 */ 619 private void modifyVariablesStack(TypeDeclDesc obtainedClass, 620 Deque<VariableDesc> variablesStack, 621 DetailAST literalNewAst) { 622 if (obtainedClass != null) { 623 final Deque<VariableDesc> instAndClassVarDeque = typeDeclAstToTypeDeclDesc 624 .get(obtainedClass.getTypeDeclAst()) 625 .getUpdatedCopyOfVarStack(literalNewAst); 626 instAndClassVarDeque.forEach(variablesStack::push); 627 } 628 } 629 630 /** 631 * Checks if there is a type declaration with same name as the super class. 632 * 633 * @param superClassName name of the super class 634 * @return list if there is another type declaration with same name. 635 */ 636 private List<TypeDeclDesc> typeDeclWithSameName(String superClassName) { 637 return typeDeclAstToTypeDeclDesc.values().stream() 638 .filter(typeDeclDesc -> { 639 return hasSameNameAsSuperClass(superClassName, typeDeclDesc); 640 }) 641 .toList(); 642 } 643 644 /** 645 * Whether the qualified name of {@code typeDeclDesc} matches the super class name. 646 * 647 * @param superClassName name of the super class 648 * @param typeDeclDesc type declaration description 649 * @return {@code true} if the qualified name of {@code typeDeclDesc} 650 * matches the super class name 651 */ 652 private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc typeDeclDesc) { 653 final boolean result; 654 if (packageName == null && typeDeclDesc.getDepth() == 0) { 655 result = typeDeclDesc.getQualifiedName().equals(superClassName); 656 } 657 else { 658 result = typeDeclDesc.getQualifiedName() 659 .endsWith(PACKAGE_SEPARATOR + superClassName); 660 } 661 return result; 662 } 663 664 /** 665 * For all type declarations with the same name as the superclass, gets the nearest type 666 * declaration. 667 * 668 * @param outerTypeDeclName outer type declaration of anonymous inner class 669 * @param typeDeclWithSameName typeDeclarations which have the same name as the super class 670 * @return the nearest class 671 */ 672 private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName, 673 List<TypeDeclDesc> typeDeclWithSameName) { 674 return Collections.min(typeDeclWithSameName, (first, second) -> { 675 return calculateTypeDeclarationDistance(outerTypeDeclName, first, second); 676 }); 677 } 678 679 /** 680 * Get the difference between type declaration name matching count. If the 681 * difference between them is zero, then their depth is compared to obtain the result. 682 * 683 * @param outerTypeName outer type declaration of anonymous inner class 684 * @param firstType first input type declaration 685 * @param secondType second input type declaration 686 * @return difference between type declaration name matching count 687 */ 688 private static int calculateTypeDeclarationDistance(String outerTypeName, 689 TypeDeclDesc firstType, 690 TypeDeclDesc secondType) { 691 final int firstMatchCount = 692 countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName()); 693 final int secondMatchCount = 694 countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName()); 695 final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount); 696 697 final int distance; 698 if (matchDistance == 0) { 699 distance = Integer.compare(firstType.getDepth(), secondType.getDepth()); 700 } 701 else { 702 distance = matchDistance; 703 } 704 705 return distance; 706 } 707 708 /** 709 * Calculates the type declaration matching count for the superclass of an anonymous inner 710 * class. 711 * 712 * <p> 713 * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is 714 * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by 715 * comparing the characters at each position, and updating the count whenever a '.' 716 * is encountered. 717 * This is necessary because pattern class can include anonymous inner classes, unlike regular 718 * inheritance where nested classes cannot be extended. 719 * </p> 720 * 721 * @param pattern type declaration to match against 722 * @param candidate type declaration to be matched 723 * @return the type declaration matching count 724 */ 725 private static int countMatchingQualifierChars(String pattern, 726 String candidate) { 727 final int typeDeclarationToBeMatchedLength = candidate.length(); 728 final int minLength = Math 729 .min(typeDeclarationToBeMatchedLength, pattern.length()); 730 final boolean shouldCountBeUpdatedAtLastCharacter = 731 typeDeclarationToBeMatchedLength > minLength 732 && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0); 733 734 int result = 0; 735 for (int idx = 0; 736 idx < minLength 737 && pattern.charAt(idx) == candidate.charAt(idx); 738 idx++) { 739 740 if (shouldCountBeUpdatedAtLastCharacter 741 || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) { 742 result = idx; 743 } 744 } 745 return result; 746 } 747 748 /** 749 * Get qualified type declaration name from type ast. 750 * 751 * @param typeDeclAst type declaration ast 752 * @return qualified name of type declaration 753 */ 754 private String getQualifiedTypeDeclarationName(DetailAST typeDeclAst) { 755 final String className = typeDeclAst.findFirstToken(TokenTypes.IDENT).getText(); 756 String outerClassQualifiedName = null; 757 if (!typeDeclarations.isEmpty()) { 758 outerClassQualifiedName = typeDeclarations.peek().getQualifiedName(); 759 } 760 return CheckUtil 761 .getQualifiedTypeDeclarationName(packageName, outerClassQualifiedName, className); 762 } 763 764 /** 765 * Iterate over all the ast nodes present under {@code ast}. 766 * 767 * @param ast ast 768 * @param variablesStack stack of all the relevant variables in the scope 769 */ 770 private void iterateOverBlockContainingLocalAnonInnerClass( 771 DetailAST ast, Deque<VariableDesc> variablesStack) { 772 DetailAST currNode = ast; 773 while (currNode != null) { 774 customVisitToken(currNode, variablesStack); 775 DetailAST toVisit = currNode.getFirstChild(); 776 while (currNode != ast && toVisit == null) { 777 customLeaveToken(currNode, variablesStack); 778 toVisit = currNode.getNextSibling(); 779 currNode = currNode.getParent(); 780 } 781 currNode = toVisit; 782 } 783 } 784 785 /** 786 * Visit all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once 787 * again. 788 * 789 * @param ast ast 790 * @param variablesStack stack of all the relevant variables in the scope 791 */ 792 private void customVisitToken(DetailAST ast, Deque<VariableDesc> variablesStack) { 793 final int type = ast.getType(); 794 switch (type) { 795 case TokenTypes.DOT -> visitDotToken(ast, variablesStack); 796 797 case TokenTypes.VARIABLE_DEF -> addLocalVariables(ast, variablesStack); 798 799 case TokenTypes.IDENT -> visitIdentToken(ast, variablesStack); 800 801 case TokenTypes.LITERAL_NEW -> { 802 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { 803 final TypeDeclDesc obtainedClass = getSuperClassOfAnonInnerClass(ast); 804 modifyVariablesStack(obtainedClass, variablesStack, ast); 805 } 806 } 807 808 default -> { 809 // No action needed for other token types 810 } 811 } 812 } 813 814 /** 815 * Leave all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once 816 * again. 817 * 818 * @param ast ast 819 * @param variablesStack stack of all the relevant variables in the scope 820 */ 821 private void customLeaveToken(DetailAST ast, Deque<VariableDesc> variablesStack) { 822 logViolations(ast, variablesStack); 823 } 824 825 /** 826 * Whether to check identifier token nested under dotAst. 827 * 828 * @param dotAst dotAst 829 * @return true if ident nested under dotAst should be checked 830 */ 831 private static boolean shouldCheckIdentTokenNestedUnderDot(DetailAST dotAst) { 832 833 return TokenUtil.findFirstTokenByPredicate(dotAst, 834 childAst -> { 835 return TokenUtil.isOfType(childAst, 836 UNACCEPTABLE_CHILD_OF_DOT); 837 }) 838 .isEmpty(); 839 } 840 841 /** 842 * Checks the identifier ast. 843 * 844 * @param identAst ast of type {@link TokenTypes#IDENT} 845 * @param variablesStack stack of all the relevant variables in the scope 846 */ 847 private static void checkIdentifierAst(DetailAST identAst, Deque<VariableDesc> variablesStack) { 848 for (VariableDesc variableDesc : variablesStack) { 849 if (identAst.getText().equals(variableDesc.getName()) 850 && !isLeftHandSideValue(identAst)) { 851 variableDesc.registerAsUsed(); 852 break; 853 } 854 } 855 } 856 857 /** 858 * Find the scope of variable. 859 * 860 * @param variableDef ast of type {@link TokenTypes#VARIABLE_DEF} 861 * @return scope of variableDef 862 */ 863 private static DetailAST findScopeOfVariable(DetailAST variableDef) { 864 final DetailAST result; 865 final DetailAST parentAst = variableDef.getParent(); 866 if (TokenUtil.isOfType(parentAst, TokenTypes.SLIST, TokenTypes.OBJBLOCK)) { 867 result = parentAst; 868 } 869 else { 870 result = parentAst.getParent(); 871 } 872 return result; 873 } 874 875 /** 876 * Checks whether the ast of type {@link TokenTypes#IDENT} is 877 * used as left-hand side value. An identifier is being used as a left-hand side 878 * value if it is used as the left operand of an assignment or as an 879 * operand of a stand-alone increment or decrement. 880 * 881 * @param identAst ast of type {@link TokenTypes#IDENT} 882 * @return true if identAst is used as a left-hand side value 883 */ 884 private static boolean isLeftHandSideValue(DetailAST identAst) { 885 final DetailAST parent = identAst.getParent(); 886 return isStandAloneIncrementOrDecrement(identAst) 887 || parent.getType() == TokenTypes.ASSIGN 888 && identAst != parent.getLastChild(); 889 } 890 891 /** 892 * Checks whether the ast of type {@link TokenTypes#IDENT} is used as 893 * an operand of a stand-alone increment or decrement. 894 * 895 * @param identAst ast of type {@link TokenTypes#IDENT} 896 * @return true if identAst is used as an operand of stand-alone 897 * increment or decrement 898 */ 899 private static boolean isStandAloneIncrementOrDecrement(DetailAST identAst) { 900 final DetailAST parent = identAst.getParent(); 901 final DetailAST grandParent = parent.getParent(); 902 return TokenUtil.isOfType(parent, INCREMENT_AND_DECREMENT_TOKENS) 903 && TokenUtil.isOfType(grandParent, TokenTypes.EXPR) 904 && !isIncrementOrDecrementVariableUsed(grandParent); 905 } 906 907 /** 908 * A variable with increment or decrement operator is considered used if it 909 * is used as an argument or as an array index or for assigning value 910 * to a variable. 911 * 912 * @param exprAst ast of type {@link TokenTypes#EXPR} 913 * @return true if variable nested in exprAst is used 914 */ 915 private static boolean isIncrementOrDecrementVariableUsed(DetailAST exprAst) { 916 return TokenUtil.isOfType(exprAst.getParent(), INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES) 917 && exprAst.getParent().getParent().getType() != TokenTypes.FOR_ITERATOR; 918 } 919 920 /** 921 * Maintains information about the variable. 922 */ 923 private static final class VariableDesc { 924 925 /** 926 * The name of the variable. 927 */ 928 private final String name; 929 930 /** 931 * Ast of type {@link TokenTypes#TYPE}. 932 */ 933 private final DetailAST typeAst; 934 935 /** 936 * The scope of variable is determined by the ast of type 937 * {@link TokenTypes#SLIST} or {@link TokenTypes#LITERAL_FOR} 938 * or {@link TokenTypes#OBJBLOCK} which is enclosing the variable. 939 */ 940 private final DetailAST scope; 941 942 /** 943 * Is an instance variable or a class variable. 944 */ 945 private boolean instVarOrClassVar; 946 947 /** 948 * Is the variable used. 949 */ 950 private boolean used; 951 952 /** 953 * Create a new VariableDesc instance. 954 * 955 * @param name name of the variable 956 */ 957 private VariableDesc(String name) { 958 this(name, null, null); 959 } 960 961 /** 962 * Create a new VariableDesc instance. 963 * 964 * @param name name of the variable 965 * @param scope ast of type {@link TokenTypes#SLIST} or 966 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 967 * which is enclosing the variable 968 */ 969 private VariableDesc(String name, DetailAST scope) { 970 this(name, null, scope); 971 } 972 973 /** 974 * Create a new VariableDesc instance. 975 * 976 * @param name name of the variable 977 * @param typeAst ast of type {@link TokenTypes#TYPE} 978 * @param scope ast of type {@link TokenTypes#SLIST} or 979 * {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 980 * which is enclosing the variable 981 */ 982 private VariableDesc(String name, DetailAST typeAst, DetailAST scope) { 983 this.name = name; 984 this.typeAst = typeAst; 985 this.scope = scope; 986 } 987 988 /** 989 * Get the name of variable. 990 * 991 * @return name of variable 992 */ 993 /* package */ String getName() { 994 return name; 995 } 996 997 /** 998 * Get the associated ast node of type {@link TokenTypes#TYPE}. 999 * 1000 * @return the associated ast node of type {@link TokenTypes#TYPE} 1001 */ 1002 /* package */ DetailAST getTypeAst() { 1003 return typeAst; 1004 } 1005 1006 /** 1007 * Get ast of type {@link TokenTypes#SLIST} 1008 * or {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK} 1009 * which is enclosing the variable i.e. its scope. 1010 * 1011 * @return the scope associated with the variable 1012 */ 1013 /* package */ DetailAST getScope() { 1014 return scope; 1015 } 1016 1017 /** 1018 * Register the variable as used. 1019 */ 1020 /* package */ void registerAsUsed() { 1021 used = true; 1022 } 1023 1024 /** 1025 * Register the variable as an instance variable or 1026 * class variable. 1027 */ 1028 /* package */ void registerAsInstOrClassVar() { 1029 instVarOrClassVar = true; 1030 } 1031 1032 /** 1033 * Is the variable used or not. 1034 * 1035 * @return true if variable is used 1036 */ 1037 /* package */ boolean isUsed() { 1038 return used; 1039 } 1040 1041 /** 1042 * Is an instance variable or a class variable. 1043 * 1044 * @return true if is an instance variable or a class variable 1045 */ 1046 /* package */ boolean isInstVarOrClassVar() { 1047 return instVarOrClassVar; 1048 } 1049 } 1050 1051 /** 1052 * Maintains information about the type declaration. 1053 * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF} 1054 * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF} 1055 * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration. 1056 */ 1057 private static final class TypeDeclDesc { 1058 1059 /** 1060 * Complete type declaration name with package name and outer type declaration name. 1061 */ 1062 private final String qualifiedName; 1063 1064 /** 1065 * Depth of nesting of type declaration. 1066 */ 1067 private final int depth; 1068 1069 /** 1070 * Type declaration ast node. 1071 */ 1072 private final DetailAST typeDeclAst; 1073 1074 /** 1075 * A stack of type declaration's instance and static variables. 1076 */ 1077 private final Deque<VariableDesc> instanceAndClassVarStack; 1078 1079 /** 1080 * Create a new TypeDeclDesc instance. 1081 * 1082 * @param qualifiedName qualified name 1083 * @param depth depth of nesting 1084 * @param typeDeclAst type declaration ast node 1085 */ 1086 private TypeDeclDesc(String qualifiedName, int depth, 1087 DetailAST typeDeclAst) { 1088 this.qualifiedName = qualifiedName; 1089 this.depth = depth; 1090 this.typeDeclAst = typeDeclAst; 1091 instanceAndClassVarStack = new ArrayDeque<>(); 1092 } 1093 1094 /** 1095 * Get the complete type declaration name i.e. type declaration name with package name 1096 * and outer type declaration name. 1097 * 1098 * @return qualified class name 1099 */ 1100 /* package */ String getQualifiedName() { 1101 return qualifiedName; 1102 } 1103 1104 /** 1105 * Get the depth of type declaration. 1106 * 1107 * @return the depth of nesting of type declaration 1108 */ 1109 /* package */ int getDepth() { 1110 return depth; 1111 } 1112 1113 /** 1114 * Get the type declaration ast node. 1115 * 1116 * @return ast node of the type declaration 1117 */ 1118 /* package */ DetailAST getTypeDeclAst() { 1119 return typeDeclAst; 1120 } 1121 1122 /** 1123 * Get the copy of variables in instanceAndClassVar stack with updated scope. 1124 * 1125 * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW} 1126 * @return copy of variables in instanceAndClassVar stack with updated scope. 1127 */ 1128 /* package */ Deque<VariableDesc> getUpdatedCopyOfVarStack(DetailAST literalNewAst) { 1129 final DetailAST updatedScope = literalNewAst; 1130 final Deque<VariableDesc> instAndClassVarDeque = new ArrayDeque<>(); 1131 instanceAndClassVarStack.forEach(instVar -> { 1132 final VariableDesc variableDesc = new VariableDesc(instVar.getName(), 1133 updatedScope); 1134 variableDesc.registerAsInstOrClassVar(); 1135 instAndClassVarDeque.push(variableDesc); 1136 }); 1137 return instAndClassVarDeque; 1138 } 1139 1140 /** 1141 * Add an instance variable or class variable to the stack. 1142 * 1143 * @param variableDesc variable to be added 1144 */ 1145 /* package */ void addInstOrClassVar(VariableDesc variableDesc) { 1146 instanceAndClassVarStack.push(variableDesc); 1147 } 1148 } 1149}