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