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