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