View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.ArrayDeque;
23  import java.util.Collections;
24  import java.util.Deque;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Optional;
32  import java.util.Set;
33  
34  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
35  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
36  import com.puppycrawl.tools.checkstyle.api.DetailAST;
37  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
39  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
40  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
41  
42  /**
43   * <div>
44   * Checks that a local variable is declared and/or assigned, but not used.
45   * Supports
46   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.30">
47   * pattern variables</a>.
48   * Doesn't check
49   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.12.3">
50   * array components</a> as array
51   * components are classified as different kind of variables by
52   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/index.html">JLS</a>.
53   * </div>
54   *
55   * @since 9.3
56   */
57  @FileStatefulCheck
58  public class UnusedLocalVariableCheck extends AbstractCheck {
59  
60      /**
61       * A key is pointing to the warning message text in "messages.properties"
62       * file.
63       */
64      public static final String MSG_UNUSED_LOCAL_VARIABLE = "unused.local.var";
65  
66      /**
67       * A key is pointing to the warning message text in "messages.properties"
68       * file.
69       */
70      public static final String MSG_UNUSED_NAMED_LOCAL_VARIABLE = "unused.named.local.var";
71  
72      /**
73       * An array of increment and decrement tokens.
74       */
75      private static final int[] INCREMENT_AND_DECREMENT_TOKENS = {
76          TokenTypes.POST_INC,
77          TokenTypes.POST_DEC,
78          TokenTypes.INC,
79          TokenTypes.DEC,
80      };
81  
82      /**
83       * An array of scope tokens.
84       */
85      private static final int[] SCOPES = {
86          TokenTypes.SLIST,
87          TokenTypes.LITERAL_FOR,
88          TokenTypes.OBJBLOCK,
89      };
90  
91      /**
92       * An array of unacceptable children of ast of type {@link TokenTypes#DOT}.
93       */
94      private static final int[] UNACCEPTABLE_CHILD_OF_DOT = {
95          TokenTypes.DOT,
96          TokenTypes.METHOD_CALL,
97          TokenTypes.LITERAL_NEW,
98          TokenTypes.LITERAL_SUPER,
99          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      * 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 and instanceof
233      * record Destructuring cannot be replaced with {@code _}, so violations
234      * on them are suppressed when jdkVersion is set 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 (isForcedNamePatternVariable(patternVarDefAst)) {
446             desc.registerAsNamedPatternVar();
447         }
448         variablesStack.push(desc);
449     }
450 
451     /**
452      * Checks whether the pattern variable is declared in a switch labels and instanceof.
453      *
454      * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
455      * @return true if the pattern variable is in a forced-name context
456      */
457     private static boolean isForcedNamePatternVariable(DetailAST patternVarDefAst) {
458         return patternVarDefAst.getParent().getType() != TokenTypes.LITERAL_INSTANCEOF;
459     }
460 
461     /**
462      * Find the scope of a pattern variable.
463      *
464      * @param patternVarDefAst ast of type.
465      * @return the outermost enclosing {@link TokenTypes#SLIST}, or {@code null} if none.
466      */
467     private static DetailAST findScopeOfPatternVariable(DetailAST patternVarDefAst) {
468         final Deque<DetailAST> slistAncestors = new ArrayDeque<>();
469         for (DetailAST current = patternVarDefAst;
470              current != null;
471              current = current.getParent()) {
472             if (current.getType() == TokenTypes.SLIST) {
473                 slistAncestors.push(current);
474             }
475         }
476         return slistAncestors.peekLast();
477     }
478 
479     /**
480      * Whether ast node of type {@link TokenTypes#LITERAL_NEW} is a part of a local
481      * anonymous inner class.
482      *
483      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
484      * @return true if variableDefAst is an instance variable in local anonymous inner class
485      */
486     private static boolean isInsideLocalAnonInnerClass(DetailAST literalNewAst) {
487         boolean result = false;
488         final DetailAST lastChild = literalNewAst.getLastChild();
489         if (lastChild != null && lastChild.getType() == TokenTypes.OBJBLOCK) {
490             DetailAST currentAst = literalNewAst;
491             while (currentAst != null
492                     && !TokenUtil.isTypeDeclaration(currentAst.getType())) {
493                 if (currentAst.getType() == TokenTypes.SLIST) {
494                     result = true;
495                     break;
496                 }
497                 currentAst = currentAst.getParent();
498             }
499         }
500         return result;
501     }
502 
503     /**
504      * Traverse {@code variablesStack} stack and log the violations.
505      *
506      * @param scopeAst ast node of type {@link UnusedLocalVariableCheck#SCOPES}
507      * @param variablesStack stack of all the relevant variables in the scope
508      */
509     private void logViolations(DetailAST scopeAst, Deque<VariableDesc> variablesStack) {
510         final Iterator<VariableDesc> iterator = variablesStack.iterator();
511         while (iterator.hasNext()) {
512             final VariableDesc variableDesc = iterator.next();
513             if (variableDesc.getScope() == scopeAst) {
514                 iterator.remove();
515                 if (!variableDesc.isUsed()
516                         && !variableDesc.isInstVarOrClassVar()
517                         && !(jdkVersion < JDK_22
518                                 && variableDesc.isNamedPatternVar())) {
519                     final DetailAST typeAst = variableDesc.getTypeAst();
520                     if (allowUnnamedVariables) {
521                         log(typeAst, MSG_UNUSED_NAMED_LOCAL_VARIABLE, variableDesc.getName());
522                     }
523                     else {
524                         log(typeAst, MSG_UNUSED_LOCAL_VARIABLE, variableDesc.getName());
525                     }
526                 }
527             }
528         }
529     }
530 
531     /**
532      * We process all the blocks containing local anonymous inner classes
533      * separately after processing all the other nodes. This is being done
534      * due to the fact the instance variables of local anon inner classes can
535      * cast a shadow on local variables.
536      */
537     private void leaveCompilationUnit() {
538         anonInnerClassHolders.forEach(holder -> {
539             iterateOverBlockContainingLocalAnonInnerClass(holder, new ArrayDeque<>());
540         });
541     }
542 
543     /**
544      * Whether a type declaration is non-local. Annotated interfaces are always non-local.
545      *
546      * @param typeDeclAst type declaration ast
547      * @return true if type declaration is non-local
548      */
549     private static boolean isNonLocalTypeDeclaration(DetailAST typeDeclAst) {
550         return TokenUtil.isTypeDeclaration(typeDeclAst.getType())
551                 && typeDeclAst.getParent().getType() != TokenTypes.SLIST;
552     }
553 
554     /**
555      * Get the block containing local anon inner class.
556      *
557      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
558      * @return the block containing local anon inner class
559      */
560     private static DetailAST getBlockContainingLocalAnonInnerClass(DetailAST literalNewAst) {
561         DetailAST currentAst = literalNewAst;
562         DetailAST result = null;
563         DetailAST topMostLambdaAst = null;
564         boolean continueSearch = true;
565         while (continueSearch) {
566             continueSearch = false;
567             while (currentAst != null
568                     && !TokenUtil.isOfType(currentAst, ANONYMOUS_CLASS_PARENT_TOKENS)) {
569                 if (currentAst.getType() == TokenTypes.LAMBDA) {
570                     topMostLambdaAst = currentAst;
571                     currentAst = currentAst.getParent();
572                     continueSearch = true;
573                     break;
574                 }
575                 currentAst = currentAst.getParent();
576                 result = currentAst;
577             }
578         }
579 
580         if (currentAst == null) {
581             result = topMostLambdaAst;
582         }
583         return result;
584     }
585 
586     /**
587      * Add local variables to the {@code variablesStack} stack.
588      * Also adds the instance variables defined in a local anonymous inner class.
589      *
590      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
591      * @param variablesStack stack of all the relevant variables in the scope
592      */
593     private static void addLocalVariables(DetailAST varDefAst, Deque<VariableDesc> variablesStack) {
594         final DetailAST parentAst = varDefAst.getParent();
595         final DetailAST grandParent = parentAst.getParent();
596 
597         if (grandParent != null) {
598             final boolean isInstanceVarInInnerClass =
599                     grandParent.getType() == TokenTypes.LITERAL_NEW
600                     || grandParent.getType() == TokenTypes.CLASS_DEF;
601             if (isInstanceVarInInnerClass
602                     || parentAst.getType() != TokenTypes.OBJBLOCK) {
603                 final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
604                 final VariableDesc desc = new VariableDesc(ident.getText(),
605                         varDefAst.findFirstToken(TokenTypes.TYPE), findScopeOfVariable(varDefAst));
606                 if (isInstanceVarInInnerClass) {
607                     desc.registerAsInstOrClassVar();
608                 }
609                 variablesStack.push(desc);
610             }
611         }
612     }
613 
614     /**
615      * Add instance variables and class variables to the
616      * {@link TypeDeclDesc#instanceAndClassVarStack}.
617      *
618      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
619      */
620     private void addInstanceOrClassVar(DetailAST varDefAst) {
621         final DetailAST parentAst = varDefAst.getParent();
622         final DetailAST grandParentAst = parentAst.getParent();
623         if (grandParentAst != null
624                 && isNonLocalTypeDeclaration(grandParentAst)
625                 && !isPrivateInstanceVariable(varDefAst)) {
626             final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
627             final VariableDesc desc = new VariableDesc(ident.getText());
628             typeDeclAstToTypeDeclDesc.get(grandParentAst).addInstOrClassVar(desc);
629         }
630     }
631 
632     /**
633      * Whether instance variable or class variable have private access modifier.
634      *
635      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
636      * @return true if instance variable or class variable have private access modifier
637      */
638     private static boolean isPrivateInstanceVariable(DetailAST varDefAst) {
639         final AccessModifierOption varAccessModifier =
640                 CheckUtil.getAccessModifierFromModifiersToken(varDefAst);
641         return varAccessModifier == AccessModifierOption.PRIVATE;
642     }
643 
644     /**
645      * Get the {@link TypeDeclDesc} of the super class of anonymous inner class.
646      *
647      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
648      * @return {@link TypeDeclDesc} of the super class of anonymous inner class
649      */
650     private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) {
651         TypeDeclDesc obtainedClass = null;
652         final String shortNameOfClass = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst);
653         if (packageName != null && shortNameOfClass.startsWith(packageName)) {
654             final Optional<TypeDeclDesc> classWithCompletePackageName =
655                     typeDeclAstToTypeDeclDesc.values()
656                     .stream()
657                     .filter(typeDeclDesc -> {
658                         return typeDeclDesc.getQualifiedName().equals(shortNameOfClass);
659                     })
660                     .findFirst();
661             if (classWithCompletePackageName.isPresent()) {
662                 obtainedClass = classWithCompletePackageName.orElseThrow();
663             }
664         }
665         else {
666             final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass);
667             if (!typeDeclWithSameName.isEmpty()) {
668                 obtainedClass = getClosestMatchingTypeDeclaration(
669                         anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(),
670                         typeDeclWithSameName);
671             }
672         }
673         return obtainedClass;
674     }
675 
676     /**
677      * Add non-private instance and class variables of the super class of the anonymous class
678      * to the variables stack.
679      *
680      * @param obtainedClass super class of the anon inner class
681      * @param variablesStack stack of all the relevant variables in the scope
682      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
683      */
684     private void modifyVariablesStack(TypeDeclDesc obtainedClass,
685             Deque<VariableDesc> variablesStack,
686             DetailAST literalNewAst) {
687         if (obtainedClass != null) {
688             final Deque<VariableDesc> instAndClassVarDeque = typeDeclAstToTypeDeclDesc
689                     .get(obtainedClass.getTypeDeclAst())
690                     .getUpdatedCopyOfVarStack(literalNewAst);
691             instAndClassVarDeque.forEach(variablesStack::push);
692         }
693     }
694 
695     /**
696      * Checks if there is a type declaration with same name as the super class.
697      *
698      * @param superClassName name of the super class
699      * @return list if there is another type declaration with same name.
700      */
701     private List<TypeDeclDesc> typeDeclWithSameName(String superClassName) {
702         return typeDeclAstToTypeDeclDesc.values().stream()
703                 .filter(typeDeclDesc -> {
704                     return hasSameNameAsSuperClass(superClassName, typeDeclDesc);
705                 })
706                 .toList();
707     }
708 
709     /**
710      * Whether the qualified name of {@code typeDeclDesc} matches the super class name.
711      *
712      * @param superClassName name of the super class
713      * @param typeDeclDesc type declaration description
714      * @return {@code true} if the qualified name of {@code typeDeclDesc}
715      *         matches the super class name
716      */
717     private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc typeDeclDesc) {
718         final boolean result;
719         if (packageName == null && typeDeclDesc.getDepth() == 0) {
720             result = typeDeclDesc.getQualifiedName().equals(superClassName);
721         }
722         else {
723             result = typeDeclDesc.getQualifiedName()
724                     .endsWith(PACKAGE_SEPARATOR + superClassName);
725         }
726         return result;
727     }
728 
729     /**
730      * For all type declarations with the same name as the superclass, gets the nearest type
731      * declaration.
732      *
733      * @param outerTypeDeclName outer type declaration of anonymous inner class
734      * @param typeDeclWithSameName typeDeclarations which have the same name as the super class
735      * @return the nearest class
736      */
737     private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName,
738             List<TypeDeclDesc> typeDeclWithSameName) {
739         return Collections.min(typeDeclWithSameName, (first, second) -> {
740             return calculateTypeDeclarationDistance(outerTypeDeclName, first, second);
741         });
742     }
743 
744     /**
745      * Get the difference between type declaration name matching count. If the
746      * difference between them is zero, then their depth is compared to obtain the result.
747      *
748      * @param outerTypeName outer type declaration of anonymous inner class
749      * @param firstType first input type declaration
750      * @param secondType second input type declaration
751      * @return difference between type declaration name matching count
752      */
753     private static int calculateTypeDeclarationDistance(String outerTypeName,
754                                                         TypeDeclDesc firstType,
755                                                         TypeDeclDesc secondType) {
756         final int firstMatchCount =
757                 countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName());
758         final int secondMatchCount =
759                 countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName());
760         final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount);
761 
762         final int distance;
763         if (matchDistance == 0) {
764             distance = Integer.compare(firstType.getDepth(), secondType.getDepth());
765         }
766         else {
767             distance = matchDistance;
768         }
769 
770         return distance;
771     }
772 
773     /**
774      * Calculates the type declaration matching count for the superclass of an anonymous inner
775      * class.
776      *
777      * <p>
778      * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is
779      * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by
780      * comparing the characters at each position, and updating the count whenever a '.'
781      * is encountered.
782      * This is necessary because pattern class can include anonymous inner classes, unlike regular
783      * inheritance where nested classes cannot be extended.
784      * </p>
785      *
786      * @param pattern type declaration to match against
787      * @param candidate type declaration to be matched
788      * @return the type declaration matching count
789      */
790     private static int countMatchingQualifierChars(String pattern,
791                                                    String candidate) {
792         final int typeDeclarationToBeMatchedLength = candidate.length();
793         final int minLength = Math
794                 .min(typeDeclarationToBeMatchedLength, pattern.length());
795         final boolean shouldCountBeUpdatedAtLastCharacter =
796                 typeDeclarationToBeMatchedLength > minLength
797                 && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0);
798 
799         int result = 0;
800         for (int idx = 0;
801              idx < minLength
802                 && pattern.charAt(idx) == candidate.charAt(idx);
803              idx++) {
804 
805             if (shouldCountBeUpdatedAtLastCharacter
806                     || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) {
807                 result = idx;
808             }
809         }
810         return result;
811     }
812 
813     /**
814      * Get qualified type declaration name from type ast.
815      *
816      * @param typeDeclAst type declaration ast
817      * @return qualified name of type declaration
818      */
819     private String getQualifiedTypeDeclarationName(DetailAST typeDeclAst) {
820         final String className = typeDeclAst.findFirstToken(TokenTypes.IDENT).getText();
821         String outerClassQualifiedName = null;
822         if (!typeDeclarations.isEmpty()) {
823             outerClassQualifiedName = typeDeclarations.peek().getQualifiedName();
824         }
825         return CheckUtil
826             .getQualifiedTypeDeclarationName(packageName, outerClassQualifiedName, className);
827     }
828 
829     /**
830      * Iterate over all the ast nodes present under {@code ast}.
831      *
832      * @param ast ast
833      * @param variablesStack stack of all the relevant variables in the scope
834      */
835     private void iterateOverBlockContainingLocalAnonInnerClass(
836             DetailAST ast, Deque<VariableDesc> variablesStack) {
837         DetailAST currNode = ast;
838         while (currNode != null) {
839             customVisitToken(currNode, variablesStack);
840             DetailAST toVisit = currNode.getFirstChild();
841             while (currNode != ast && toVisit == null) {
842                 customLeaveToken(currNode, variablesStack);
843                 toVisit = currNode.getNextSibling();
844                 currNode = currNode.getParent();
845             }
846             currNode = toVisit;
847         }
848     }
849 
850     /**
851      * Visit all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
852      * again.
853      *
854      * @param ast ast
855      * @param variablesStack stack of all the relevant variables in the scope
856      */
857     private void customVisitToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
858         final int type = ast.getType();
859         switch (type) {
860             case TokenTypes.DOT -> visitDotToken(ast, variablesStack);
861 
862             case TokenTypes.VARIABLE_DEF -> addLocalVariables(ast, variablesStack);
863 
864             case TokenTypes.IDENT -> visitIdentToken(ast, variablesStack);
865 
866             case TokenTypes.LITERAL_NEW -> {
867                 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
868                     final TypeDeclDesc obtainedClass = getSuperClassOfAnonInnerClass(ast);
869                     modifyVariablesStack(obtainedClass, variablesStack, ast);
870                 }
871             }
872 
873             default -> {
874                 // No action needed for other token types
875             }
876         }
877     }
878 
879     /**
880      * Leave all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
881      * again.
882      *
883      * @param ast ast
884      * @param variablesStack stack of all the relevant variables in the scope
885      */
886     private void customLeaveToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
887         logViolations(ast, variablesStack);
888     }
889 
890     /**
891      * Whether to check identifier token nested under dotAst.
892      *
893      * @param dotAst dotAst
894      * @return true if ident nested under dotAst should be checked
895      */
896     private static boolean shouldCheckIdentTokenNestedUnderDot(DetailAST dotAst) {
897 
898         return TokenUtil.findFirstTokenByPredicate(dotAst,
899                         childAst -> {
900                             return TokenUtil.isOfType(childAst,
901                                     UNACCEPTABLE_CHILD_OF_DOT);
902                         })
903                 .isEmpty();
904     }
905 
906     /**
907      * Checks the identifier ast.
908      *
909      * @param identAst ast of type {@link TokenTypes#IDENT}
910      * @param variablesStack stack of all the relevant variables in the scope
911      */
912     private static void checkIdentifierAst(DetailAST identAst, Deque<VariableDesc> variablesStack) {
913         for (VariableDesc variableDesc : variablesStack) {
914             if (identAst.getText().equals(variableDesc.getName())
915                     && !isLeftHandSideValue(identAst)) {
916                 variableDesc.registerAsUsed();
917                 break;
918             }
919         }
920     }
921 
922     /**
923      * Find the scope of variable.
924      *
925      * @param variableDef ast of type {@link TokenTypes#VARIABLE_DEF}
926      * @return scope of variableDef
927      */
928     private static DetailAST findScopeOfVariable(DetailAST variableDef) {
929         final DetailAST result;
930         final DetailAST parentAst = variableDef.getParent();
931         if (TokenUtil.isOfType(parentAst, TokenTypes.SLIST, TokenTypes.OBJBLOCK)) {
932             result = parentAst;
933         }
934         else {
935             result = parentAst.getParent();
936         }
937         return result;
938     }
939 
940     /**
941      * Checks whether the ast of type {@link TokenTypes#IDENT} is
942      * used as left-hand side value. An identifier is being used as a left-hand side
943      * value if it is used as the left operand of an assignment or as an
944      * operand of a stand-alone increment or decrement.
945      *
946      * @param identAst ast of type {@link TokenTypes#IDENT}
947      * @return true if identAst is used as a left-hand side value
948      */
949     private static boolean isLeftHandSideValue(DetailAST identAst) {
950         final DetailAST parent = identAst.getParent();
951         return isStandAloneIncrementOrDecrement(identAst)
952                 || parent.getType() == TokenTypes.ASSIGN
953                 && identAst != parent.getLastChild();
954     }
955 
956     /**
957      * Checks whether the ast of type {@link TokenTypes#IDENT} is used as
958      * an operand of a stand-alone increment or decrement.
959      *
960      * @param identAst ast of type {@link TokenTypes#IDENT}
961      * @return true if identAst is used as an operand of stand-alone
962      *         increment or decrement
963      */
964     private static boolean isStandAloneIncrementOrDecrement(DetailAST identAst) {
965         final DetailAST parent = identAst.getParent();
966         final DetailAST grandParent = parent.getParent();
967         return TokenUtil.isOfType(parent, INCREMENT_AND_DECREMENT_TOKENS)
968                 && TokenUtil.isOfType(grandParent, TokenTypes.EXPR)
969                 && !isIncrementOrDecrementVariableUsed(grandParent);
970     }
971 
972     /**
973      * A variable with increment or decrement operator is considered used if it
974      * is used as an argument or as an array index or for assigning value
975      * to a variable.
976      *
977      * @param exprAst ast of type {@link TokenTypes#EXPR}
978      * @return true if variable nested in exprAst is used
979      */
980     private static boolean isIncrementOrDecrementVariableUsed(DetailAST exprAst) {
981         return TokenUtil.isOfType(exprAst.getParent(), INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES)
982                 && exprAst.getParent().getParent().getType() != TokenTypes.FOR_ITERATOR;
983     }
984 
985     /**
986      * Maintains information about the variable.
987      */
988     private static final class VariableDesc {
989 
990         /**
991          * The name of the variable.
992          */
993         private final String name;
994 
995         /**
996          * Ast of type {@link TokenTypes#TYPE}.
997          */
998         private final DetailAST typeAst;
999 
1000         /**
1001          * The scope of variable is determined by the ast of type
1002          * {@link TokenTypes#SLIST} or {@link TokenTypes#LITERAL_FOR}
1003          * or {@link TokenTypes#OBJBLOCK} which is enclosing the variable.
1004          */
1005         private final DetailAST scope;
1006 
1007         /**
1008          * Is an instance variable or a class variable.
1009          */
1010         private boolean instVarOrClassVar;
1011 
1012         /**
1013          * Is a named pattern variable declared in a switch label.
1014          */
1015         private boolean namedPatternVar;
1016 
1017         /**
1018          * Is the variable used.
1019          */
1020         private boolean used;
1021 
1022         /**
1023          * Create a new VariableDesc instance.
1024          *
1025          * @param name name of the variable
1026          */
1027         private VariableDesc(String name) {
1028             this(name, null, null);
1029         }
1030 
1031         /**
1032          * Create a new VariableDesc instance.
1033          *
1034          * @param name name of the variable
1035          * @param scope ast of type {@link TokenTypes#SLIST} or
1036          *              {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1037          *              which is enclosing the variable
1038          */
1039         private VariableDesc(String name, DetailAST scope) {
1040             this(name, null, scope);
1041         }
1042 
1043         /**
1044          * Create a new VariableDesc instance.
1045          *
1046          * @param name name of the variable
1047          * @param typeAst ast of type {@link TokenTypes#TYPE}
1048          * @param scope ast of type {@link TokenTypes#SLIST} or
1049          *              {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1050          *              which is enclosing the variable
1051          */
1052         private VariableDesc(String name, DetailAST typeAst, DetailAST scope) {
1053             this.name = name;
1054             this.typeAst = typeAst;
1055             this.scope = scope;
1056         }
1057 
1058         /**
1059          * Get the name of variable.
1060          *
1061          * @return name of variable
1062          */
1063         /* package */ String getName() {
1064             return name;
1065         }
1066 
1067         /**
1068          * Get the associated ast node of type {@link TokenTypes#TYPE}.
1069          *
1070          * @return the associated ast node of type {@link TokenTypes#TYPE}
1071          */
1072         /* package */ DetailAST getTypeAst() {
1073             return typeAst;
1074         }
1075 
1076         /**
1077          * Get ast of type {@link TokenTypes#SLIST}
1078          * or {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
1079          * which is enclosing the variable i.e. its scope.
1080          *
1081          * @return the scope associated with the variable
1082          */
1083         /* package */ DetailAST getScope() {
1084             return scope;
1085         }
1086 
1087         /**
1088          * Register the variable as used.
1089          */
1090         /* package */ void registerAsUsed() {
1091             used = true;
1092         }
1093 
1094         /**
1095          * Register the variable as an instance variable or
1096          * class variable.
1097          */
1098         /* package */ void registerAsInstOrClassVar() {
1099             instVarOrClassVar = true;
1100         }
1101 
1102         /**
1103          * Register the variable as a forced-name pattern variable declared
1104          * in a switch label or instanceof record Destructuring.
1105          */
1106         /* package */ void registerAsNamedPatternVar() {
1107             namedPatternVar = true;
1108         }
1109 
1110         /**
1111          * Is the variable used or not.
1112          *
1113          * @return true if variable is used
1114          */
1115         /* package */ boolean isUsed() {
1116             return used;
1117         }
1118 
1119         /**
1120          * Is an instance variable or a class variable.
1121          *
1122          * @return true if is an instance variable or a class variable
1123          */
1124         /* package */ boolean isInstVarOrClassVar() {
1125             return instVarOrClassVar;
1126         }
1127 
1128         /**
1129          * Is a forced-name pattern variable from a switch label or
1130          * instanceof record Destructuring.
1131          *
1132          * @return true if this variable was declared in a context where
1133          *         pre-JDK 22 forces a name to be given even when unused
1134          */
1135         /* package */ boolean isNamedPatternVar() {
1136             return namedPatternVar;
1137         }
1138     }
1139 
1140     /**
1141      * Maintains information about the type declaration.
1142      * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF}
1143      * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF}
1144      * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration.
1145      */
1146     private static final class TypeDeclDesc {
1147 
1148         /**
1149          * Complete type declaration name with package name and outer type declaration name.
1150          */
1151         private final String qualifiedName;
1152 
1153         /**
1154          * Depth of nesting of type declaration.
1155          */
1156         private final int depth;
1157 
1158         /**
1159          * Type declaration ast node.
1160          */
1161         private final DetailAST typeDeclAst;
1162 
1163         /**
1164          * A stack of type declaration's instance and static variables.
1165          */
1166         private final Deque<VariableDesc> instanceAndClassVarStack;
1167 
1168         /**
1169          * Create a new TypeDeclDesc instance.
1170          *
1171          * @param qualifiedName qualified name
1172          * @param depth depth of nesting
1173          * @param typeDeclAst type declaration ast node
1174          */
1175         private TypeDeclDesc(String qualifiedName, int depth,
1176                 DetailAST typeDeclAst) {
1177             this.qualifiedName = qualifiedName;
1178             this.depth = depth;
1179             this.typeDeclAst = typeDeclAst;
1180             instanceAndClassVarStack = new ArrayDeque<>();
1181         }
1182 
1183         /**
1184          * Get the complete type declaration name i.e. type declaration name with package name
1185          * and outer type declaration name.
1186          *
1187          * @return qualified class name
1188          */
1189         /* package */ String getQualifiedName() {
1190             return qualifiedName;
1191         }
1192 
1193         /**
1194          * Get the depth of type declaration.
1195          *
1196          * @return the depth of nesting of type declaration
1197          */
1198         /* package */ int getDepth() {
1199             return depth;
1200         }
1201 
1202         /**
1203          * Get the type declaration ast node.
1204          *
1205          * @return ast node of the type declaration
1206          */
1207         /* package */ DetailAST getTypeDeclAst() {
1208             return typeDeclAst;
1209         }
1210 
1211         /**
1212          * Get the copy of variables in instanceAndClassVar stack with updated scope.
1213          *
1214          * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
1215          * @return copy of variables in instanceAndClassVar stack with updated scope.
1216          */
1217         /* package */ Deque<VariableDesc> getUpdatedCopyOfVarStack(DetailAST literalNewAst) {
1218             final DetailAST updatedScope = literalNewAst;
1219             final Deque<VariableDesc> instAndClassVarDeque = new ArrayDeque<>();
1220             instanceAndClassVarStack.forEach(instVar -> {
1221                 final VariableDesc variableDesc = new VariableDesc(instVar.getName(),
1222                         updatedScope);
1223                 variableDesc.registerAsInstOrClassVar();
1224                 instAndClassVarDeque.push(variableDesc);
1225             });
1226             return instAndClassVarDeque;
1227         }
1228 
1229         /**
1230          * Add an instance variable or class variable to the stack.
1231          *
1232          * @param variableDesc variable to be added
1233          */
1234         /* package */ void addInstOrClassVar(VariableDesc variableDesc) {
1235             instanceAndClassVarStack.push(variableDesc);
1236         }
1237     }
1238 }