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.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.Set;
32  
33  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
34  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
35  import com.puppycrawl.tools.checkstyle.api.DetailAST;
36  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
38  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
39  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
40  
41  /**
42   * <div>
43   * Checks that a local variable is declared and/or assigned, but not used.
44   * Doesn't support
45   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.30">
46   * pattern variables yet</a>.
47   * Doesn't check
48   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.12.3">
49   * array components</a> as array
50   * components are classified as different kind of variables by
51   * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/index.html">JLS</a>.
52   * </div>
53   *
54   * @since 9.3
55   */
56  @FileStatefulCheck
57  public class UnusedLocalVariableCheck extends AbstractCheck {
58  
59      /**
60       * A key is pointing to the warning message text in "messages.properties"
61       * file.
62       */
63      public static final String MSG_UNUSED_LOCAL_VARIABLE = "unused.local.var";
64  
65      /**
66       * A key is pointing to the warning message text in "messages.properties"
67       * file.
68       */
69      public static final String MSG_UNUSED_NAMED_LOCAL_VARIABLE = "unused.named.local.var";
70  
71      /**
72       * An array of increment and decrement tokens.
73       */
74      private static final int[] INCREMENT_AND_DECREMENT_TOKENS = {
75          TokenTypes.POST_INC,
76          TokenTypes.POST_DEC,
77          TokenTypes.INC,
78          TokenTypes.DEC,
79      };
80  
81      /**
82       * An array of scope tokens.
83       */
84      private static final int[] SCOPES = {
85          TokenTypes.SLIST,
86          TokenTypes.LITERAL_FOR,
87          TokenTypes.OBJBLOCK,
88      };
89  
90      /**
91       * An array of unacceptable children of ast of type {@link TokenTypes#DOT}.
92       */
93      private static final int[] UNACCEPTABLE_CHILD_OF_DOT = {
94          TokenTypes.DOT,
95          TokenTypes.METHOD_CALL,
96          TokenTypes.LITERAL_NEW,
97          TokenTypes.LITERAL_SUPER,
98          TokenTypes.LITERAL_CLASS,
99          TokenTypes.LITERAL_THIS,
100     };
101 
102     /**
103      * An array of unacceptable parent of ast of type {@link TokenTypes#IDENT}.
104      */
105     private static final int[] UNACCEPTABLE_PARENT_OF_IDENT = {
106         TokenTypes.VARIABLE_DEF,
107         TokenTypes.DOT,
108         TokenTypes.LITERAL_NEW,
109         TokenTypes.PATTERN_VARIABLE_DEF,
110         TokenTypes.METHOD_CALL,
111         TokenTypes.TYPE,
112     };
113 
114     /**
115      * An array of blocks in which local anon inner classes can exist.
116      */
117     private static final int[] ANONYMOUS_CLASS_PARENT_TOKENS = {
118         TokenTypes.METHOD_DEF,
119         TokenTypes.CTOR_DEF,
120         TokenTypes.STATIC_INIT,
121         TokenTypes.INSTANCE_INIT,
122         TokenTypes.COMPACT_CTOR_DEF,
123     };
124 
125     /**
126      * An array of token types that indicate a variable is being used within
127      * an expression involving increment or decrement operators, or within a switch statement.
128      * When a token of one of these types is the parent of an expression, it indicates that the
129      * variable associated with the increment or decrement operation is being used.
130      * Ex:- TokenTypes.LITERAL_SWITCH: Indicates a switch statement. Variables used within the
131      * switch expression are considered to be used
132      */
133     private static final int[] INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES = {
134         TokenTypes.ELIST,
135         TokenTypes.INDEX_OP,
136         TokenTypes.ASSIGN,
137         TokenTypes.LITERAL_SWITCH,
138     };
139 
140     /** Package separator. */
141     private static final String PACKAGE_SEPARATOR = ".";
142 
143     /**
144      * Keeps tracks of the variables declared in file.
145      */
146     private final Deque<VariableDesc> variables = new ArrayDeque<>();
147 
148     /**
149      * Keeps track of all the type declarations present in the file.
150      * Pops the type out of the stack while leaving the type
151      * in visitor pattern.
152      */
153     private final Deque<TypeDeclDesc> typeDeclarations = new ArrayDeque<>();
154 
155     /**
156      * Maps type declaration ast to their respective TypeDeclDesc objects.
157      */
158     private final Map<DetailAST, TypeDeclDesc> typeDeclAstToTypeDeclDesc = new LinkedHashMap<>();
159 
160     /**
161      * Maps local anonymous inner class to the TypeDeclDesc object
162      * containing it.
163      */
164     private final Map<DetailAST, TypeDeclDesc> anonInnerAstToTypeDeclDesc = new HashMap<>();
165 
166     /**
167      * Set of tokens of type {@link UnusedLocalVariableCheck#ANONYMOUS_CLASS_PARENT_TOKENS}
168      * and {@link TokenTypes#LAMBDA} in some cases.
169      */
170     private final Set<DetailAST> anonInnerClassHolders = new HashSet<>();
171 
172     /**
173      * Allow variables named with a single underscore
174      * (known as  <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
175      *  unnamed variables</a> in Java 21+).
176      */
177     private boolean allowUnnamedVariables = true;
178 
179     /**
180      * Name of the package.
181      */
182     private String packageName;
183 
184     /**
185      * Depth at which a type declaration is nested, 0 for top level type declarations.
186      */
187     private int depth;
188 
189     /**
190      * Setter to allow variables named with a single underscore
191      * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
192      * unnamed variables</a> in Java 21+).
193      *
194      * @param allowUnnamedVariables true or false.
195      * @since 10.18.0
196      */
197     public void setAllowUnnamedVariables(boolean allowUnnamedVariables) {
198         this.allowUnnamedVariables = allowUnnamedVariables;
199     }
200 
201     @Override
202     public int[] getDefaultTokens() {
203         return new int[] {
204             TokenTypes.DOT,
205             TokenTypes.VARIABLE_DEF,
206             TokenTypes.IDENT,
207             TokenTypes.SLIST,
208             TokenTypes.LITERAL_FOR,
209             TokenTypes.OBJBLOCK,
210             TokenTypes.CLASS_DEF,
211             TokenTypes.INTERFACE_DEF,
212             TokenTypes.ANNOTATION_DEF,
213             TokenTypes.PACKAGE_DEF,
214             TokenTypes.LITERAL_NEW,
215             TokenTypes.METHOD_DEF,
216             TokenTypes.CTOR_DEF,
217             TokenTypes.STATIC_INIT,
218             TokenTypes.INSTANCE_INIT,
219             TokenTypes.COMPILATION_UNIT,
220             TokenTypes.LAMBDA,
221             TokenTypes.ENUM_DEF,
222             TokenTypes.RECORD_DEF,
223             TokenTypes.COMPACT_CTOR_DEF,
224         };
225     }
226 
227     @Override
228     public int[] getAcceptableTokens() {
229         return getDefaultTokens();
230     }
231 
232     @Override
233     public int[] getRequiredTokens() {
234         return getDefaultTokens();
235     }
236 
237     @Override
238     public void beginTree(DetailAST root) {
239         variables.clear();
240         typeDeclarations.clear();
241         typeDeclAstToTypeDeclDesc.clear();
242         anonInnerAstToTypeDeclDesc.clear();
243         anonInnerClassHolders.clear();
244         packageName = null;
245         depth = 0;
246     }
247 
248     @Override
249     public void visitToken(DetailAST ast) {
250         final int type = ast.getType();
251         if (type == TokenTypes.DOT) {
252             visitDotToken(ast, variables);
253         }
254         else if (type == TokenTypes.VARIABLE_DEF && !skipUnnamedVariables(ast)) {
255             visitVariableDefToken(ast);
256         }
257         else if (type == TokenTypes.IDENT) {
258             visitIdentToken(ast, variables);
259         }
260         else if (isInsideLocalAnonInnerClass(ast)) {
261             visitLocalAnonInnerClass(ast);
262         }
263         else if (isNonLocalTypeDeclaration(ast)) {
264             visitNonLocalTypeDeclarationToken(ast);
265         }
266         else if (type == TokenTypes.PACKAGE_DEF) {
267             packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling());
268         }
269     }
270 
271     @Override
272     public void leaveToken(DetailAST ast) {
273         if (TokenUtil.isOfType(ast, SCOPES)) {
274             logViolations(ast, variables);
275         }
276         else if (ast.getType() == TokenTypes.COMPILATION_UNIT) {
277             leaveCompilationUnit();
278         }
279         else if (isNonLocalTypeDeclaration(ast)) {
280             depth--;
281             typeDeclarations.pop();
282         }
283     }
284 
285     /**
286      * Visit ast of type {@link TokenTypes#DOT}.
287      *
288      * @param dotAst dotAst
289      * @param variablesStack stack of all the relevant variables in the scope
290      */
291     private static void visitDotToken(DetailAST dotAst, Deque<VariableDesc> variablesStack) {
292         if (dotAst.getParent().getType() != TokenTypes.LITERAL_NEW
293                 && shouldCheckIdentTokenNestedUnderDot(dotAst)) {
294             final DetailAST identifier = dotAst.findFirstToken(TokenTypes.IDENT);
295             if (identifier != null) {
296                 checkIdentifierAst(identifier, variablesStack);
297             }
298         }
299     }
300 
301     /**
302      * Visit ast of type {@link TokenTypes#VARIABLE_DEF}.
303      *
304      * @param varDefAst varDefAst
305      */
306     private void visitVariableDefToken(DetailAST varDefAst) {
307         addLocalVariables(varDefAst, variables);
308         addInstanceOrClassVar(varDefAst);
309     }
310 
311     /**
312      * Visit ast of type {@link TokenTypes#IDENT}.
313      *
314      * @param identAst identAst
315      * @param variablesStack stack of all the relevant variables in the scope
316      */
317     private static void visitIdentToken(DetailAST identAst, Deque<VariableDesc> variablesStack) {
318         final DetailAST parent = identAst.getParent();
319         final boolean isMethodReferenceMethodName = parent.getType() == TokenTypes.METHOD_REF
320                 && parent.getFirstChild() != identAst;
321         final boolean isConstructorReference = parent.getType() == TokenTypes.METHOD_REF
322                 && parent.getLastChild().getType() == TokenTypes.LITERAL_NEW;
323         final boolean isNestedClassInitialization =
324                 TokenUtil.isOfType(identAst.getNextSibling(), TokenTypes.LITERAL_NEW)
325                 && parent.getType() == TokenTypes.DOT;
326 
327         if (isNestedClassInitialization || !isMethodReferenceMethodName
328                 && !isConstructorReference
329                 && !TokenUtil.isOfType(parent, UNACCEPTABLE_PARENT_OF_IDENT)) {
330             checkIdentifierAst(identAst, variablesStack);
331         }
332     }
333 
334     /**
335      * Visit the non-local type declaration token.
336      *
337      * @param typeDeclAst type declaration ast
338      */
339     private void visitNonLocalTypeDeclarationToken(DetailAST typeDeclAst) {
340         final String qualifiedName = getQualifiedTypeDeclarationName(typeDeclAst);
341         final TypeDeclDesc currTypeDecl = new TypeDeclDesc(qualifiedName, depth, typeDeclAst);
342         depth++;
343         typeDeclarations.push(currTypeDecl);
344         typeDeclAstToTypeDeclDesc.put(typeDeclAst, currTypeDecl);
345     }
346 
347     /**
348      * Visit the local anon inner class.
349      *
350      * @param literalNewAst literalNewAst
351      */
352     private void visitLocalAnonInnerClass(DetailAST literalNewAst) {
353         anonInnerAstToTypeDeclDesc.put(literalNewAst, typeDeclarations.peek());
354         anonInnerClassHolders.add(getBlockContainingLocalAnonInnerClass(literalNewAst));
355     }
356 
357     /**
358      * Check for skip current {@link TokenTypes#VARIABLE_DEF}
359      * due to <b>allowUnnamedVariable</b> option.
360      *
361      * @param varDefAst varDefAst variable to check
362      * @return true if the current variable should be skipped.
363      */
364     private boolean skipUnnamedVariables(DetailAST varDefAst) {
365         final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
366         return allowUnnamedVariables && "_".equals(ident.getText());
367     }
368 
369     /**
370      * Whether ast node of type {@link TokenTypes#LITERAL_NEW} is a part of a local
371      * anonymous inner class.
372      *
373      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
374      * @return true if variableDefAst is an instance variable in local anonymous inner class
375      */
376     private static boolean isInsideLocalAnonInnerClass(DetailAST literalNewAst) {
377         boolean result = false;
378         final DetailAST lastChild = literalNewAst.getLastChild();
379         if (lastChild != null && lastChild.getType() == TokenTypes.OBJBLOCK) {
380             DetailAST currentAst = literalNewAst;
381             while (!TokenUtil.isTypeDeclaration(currentAst.getType())) {
382                 if (currentAst.getType() == TokenTypes.SLIST) {
383                     result = true;
384                     break;
385                 }
386                 currentAst = currentAst.getParent();
387             }
388         }
389         return result;
390     }
391 
392     /**
393      * Traverse {@code variablesStack} stack and log the violations.
394      *
395      * @param scopeAst ast node of type {@link UnusedLocalVariableCheck#SCOPES}
396      * @param variablesStack stack of all the relevant variables in the scope
397      */
398     private void logViolations(DetailAST scopeAst, Deque<VariableDesc> variablesStack) {
399         while (!variablesStack.isEmpty() && variablesStack.peek().getScope() == scopeAst) {
400             final VariableDesc variableDesc = variablesStack.pop();
401             if (!variableDesc.isUsed()
402                     && !variableDesc.isInstVarOrClassVar()) {
403                 final DetailAST typeAst = variableDesc.getTypeAst();
404                 if (allowUnnamedVariables) {
405                     log(typeAst, MSG_UNUSED_NAMED_LOCAL_VARIABLE, variableDesc.getName());
406                 }
407                 else {
408                     log(typeAst, MSG_UNUSED_LOCAL_VARIABLE, variableDesc.getName());
409                 }
410             }
411         }
412     }
413 
414     /**
415      * We process all the blocks containing local anonymous inner classes
416      * separately after processing all the other nodes. This is being done
417      * due to the fact the instance variables of local anon inner classes can
418      * cast a shadow on local variables.
419      */
420     private void leaveCompilationUnit() {
421         anonInnerClassHolders.forEach(holder -> {
422             iterateOverBlockContainingLocalAnonInnerClass(holder, new ArrayDeque<>());
423         });
424     }
425 
426     /**
427      * Whether a type declaration is non-local. Annotated interfaces are always non-local.
428      *
429      * @param typeDeclAst type declaration ast
430      * @return true if type declaration is non-local
431      */
432     private static boolean isNonLocalTypeDeclaration(DetailAST typeDeclAst) {
433         return TokenUtil.isTypeDeclaration(typeDeclAst.getType())
434                 && typeDeclAst.getParent().getType() != TokenTypes.SLIST;
435     }
436 
437     /**
438      * Get the block containing local anon inner class.
439      *
440      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
441      * @return the block containing local anon inner class
442      */
443     private static DetailAST getBlockContainingLocalAnonInnerClass(DetailAST literalNewAst) {
444         DetailAST currentAst = literalNewAst;
445         DetailAST result = null;
446         DetailAST topMostLambdaAst = null;
447         boolean continueSearch = true;
448         while (continueSearch) {
449             continueSearch = false;
450             while (currentAst != null
451                     && !TokenUtil.isOfType(currentAst, ANONYMOUS_CLASS_PARENT_TOKENS)) {
452                 if (currentAst.getType() == TokenTypes.LAMBDA) {
453                     topMostLambdaAst = currentAst;
454                     currentAst = currentAst.getParent();
455                     continueSearch = true;
456                     break;
457                 }
458                 currentAst = currentAst.getParent();
459                 result = currentAst;
460             }
461         }
462 
463         if (currentAst == null) {
464             result = topMostLambdaAst;
465         }
466         return result;
467     }
468 
469     /**
470      * Add local variables to the {@code variablesStack} stack.
471      * Also adds the instance variables defined in a local anonymous inner class.
472      *
473      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
474      * @param variablesStack stack of all the relevant variables in the scope
475      */
476     private static void addLocalVariables(DetailAST varDefAst, Deque<VariableDesc> variablesStack) {
477         final DetailAST parentAst = varDefAst.getParent();
478         final DetailAST grandParent = parentAst.getParent();
479         final boolean isInstanceVarInInnerClass =
480                 grandParent.getType() == TokenTypes.LITERAL_NEW
481                 || grandParent.getType() == TokenTypes.CLASS_DEF;
482         if (isInstanceVarInInnerClass
483                 || parentAst.getType() != TokenTypes.OBJBLOCK) {
484             final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
485             final VariableDesc desc = new VariableDesc(ident.getText(),
486                     varDefAst.findFirstToken(TokenTypes.TYPE), findScopeOfVariable(varDefAst));
487             if (isInstanceVarInInnerClass) {
488                 desc.registerAsInstOrClassVar();
489             }
490             variablesStack.push(desc);
491         }
492     }
493 
494     /**
495      * Add instance variables and class variables to the
496      * {@link TypeDeclDesc#instanceAndClassVarStack}.
497      *
498      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
499      */
500     private void addInstanceOrClassVar(DetailAST varDefAst) {
501         final DetailAST parentAst = varDefAst.getParent();
502         if (isNonLocalTypeDeclaration(parentAst.getParent())
503                 && !isPrivateInstanceVariable(varDefAst)) {
504             final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
505             final VariableDesc desc = new VariableDesc(ident.getText());
506             typeDeclAstToTypeDeclDesc.get(parentAst.getParent()).addInstOrClassVar(desc);
507         }
508     }
509 
510     /**
511      * Whether instance variable or class variable have private access modifier.
512      *
513      * @param varDefAst ast node of type {@link TokenTypes#VARIABLE_DEF}
514      * @return true if instance variable or class variable have private access modifier
515      */
516     private static boolean isPrivateInstanceVariable(DetailAST varDefAst) {
517         final AccessModifierOption varAccessModifier =
518                 CheckUtil.getAccessModifierFromModifiersToken(varDefAst);
519         return varAccessModifier == AccessModifierOption.PRIVATE;
520     }
521 
522     /**
523      * Get the {@link TypeDeclDesc} of the super class of anonymous inner class.
524      *
525      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
526      * @return {@link TypeDeclDesc} of the super class of anonymous inner class
527      */
528     private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) {
529         TypeDeclDesc obtainedClass = null;
530         final String shortNameOfClass = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst);
531         if (packageName != null && shortNameOfClass.startsWith(packageName)) {
532             final Optional<TypeDeclDesc> classWithCompletePackageName =
533                     typeDeclAstToTypeDeclDesc.values()
534                     .stream()
535                     .filter(typeDeclDesc -> {
536                         return typeDeclDesc.getQualifiedName().equals(shortNameOfClass);
537                     })
538                     .findFirst();
539             if (classWithCompletePackageName.isPresent()) {
540                 obtainedClass = classWithCompletePackageName.orElseThrow();
541             }
542         }
543         else {
544             final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass);
545             if (!typeDeclWithSameName.isEmpty()) {
546                 obtainedClass = getClosestMatchingTypeDeclaration(
547                         anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(),
548                         typeDeclWithSameName);
549             }
550         }
551         return obtainedClass;
552     }
553 
554     /**
555      * Add non-private instance and class variables of the super class of the anonymous class
556      * to the variables stack.
557      *
558      * @param obtainedClass super class of the anon inner class
559      * @param variablesStack stack of all the relevant variables in the scope
560      * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
561      */
562     private void modifyVariablesStack(TypeDeclDesc obtainedClass,
563             Deque<VariableDesc> variablesStack,
564             DetailAST literalNewAst) {
565         if (obtainedClass != null) {
566             final Deque<VariableDesc> instAndClassVarDeque = typeDeclAstToTypeDeclDesc
567                     .get(obtainedClass.getTypeDeclAst())
568                     .getUpdatedCopyOfVarStack(literalNewAst);
569             instAndClassVarDeque.forEach(variablesStack::push);
570         }
571     }
572 
573     /**
574      * Checks if there is a type declaration with same name as the super class.
575      *
576      * @param superClassName name of the super class
577      * @return list if there is another type declaration with same name.
578      */
579     private List<TypeDeclDesc> typeDeclWithSameName(String superClassName) {
580         return typeDeclAstToTypeDeclDesc.values().stream()
581                 .filter(typeDeclDesc -> {
582                     return hasSameNameAsSuperClass(superClassName, typeDeclDesc);
583                 })
584                 .toList();
585     }
586 
587     /**
588      * Whether the qualified name of {@code typeDeclDesc} matches the super class name.
589      *
590      * @param superClassName name of the super class
591      * @param typeDeclDesc type declaration description
592      * @return {@code true} if the qualified name of {@code typeDeclDesc}
593      *         matches the super class name
594      */
595     private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc typeDeclDesc) {
596         final boolean result;
597         if (packageName == null && typeDeclDesc.getDepth() == 0) {
598             result = typeDeclDesc.getQualifiedName().equals(superClassName);
599         }
600         else {
601             result = typeDeclDesc.getQualifiedName()
602                     .endsWith(PACKAGE_SEPARATOR + superClassName);
603         }
604         return result;
605     }
606 
607     /**
608      * For all type declarations with the same name as the superclass, gets the nearest type
609      * declaration.
610      *
611      * @param outerTypeDeclName outer type declaration of anonymous inner class
612      * @param typeDeclWithSameName typeDeclarations which have the same name as the super class
613      * @return the nearest class
614      */
615     private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName,
616             List<TypeDeclDesc> typeDeclWithSameName) {
617         return Collections.min(typeDeclWithSameName, (first, second) -> {
618             return calculateTypeDeclarationDistance(outerTypeDeclName, first, second);
619         });
620     }
621 
622     /**
623      * Get the difference between type declaration name matching count. If the
624      * difference between them is zero, then their depth is compared to obtain the result.
625      *
626      * @param outerTypeName outer type declaration of anonymous inner class
627      * @param firstType first input type declaration
628      * @param secondType second input type declaration
629      * @return difference between type declaration name matching count
630      */
631     private static int calculateTypeDeclarationDistance(String outerTypeName,
632                                                         TypeDeclDesc firstType,
633                                                         TypeDeclDesc secondType) {
634         final int firstMatchCount =
635                 countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName());
636         final int secondMatchCount =
637                 countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName());
638         final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount);
639 
640         final int distance;
641         if (matchDistance == 0) {
642             distance = Integer.compare(firstType.getDepth(), secondType.getDepth());
643         }
644         else {
645             distance = matchDistance;
646         }
647 
648         return distance;
649     }
650 
651     /**
652      * Calculates the type declaration matching count for the superclass of an anonymous inner
653      * class.
654      *
655      * <p>
656      * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is
657      * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by
658      * comparing the characters at each position, and updating the count whenever a '.'
659      * is encountered.
660      * This is necessary because pattern class can include anonymous inner classes, unlike regular
661      * inheritance where nested classes cannot be extended.
662      * </p>
663      *
664      * @param pattern type declaration to match against
665      * @param candidate type declaration to be matched
666      * @return the type declaration matching count
667      */
668     private static int countMatchingQualifierChars(String pattern,
669                                                    String candidate) {
670         final int typeDeclarationToBeMatchedLength = candidate.length();
671         final int minLength = Math
672                 .min(typeDeclarationToBeMatchedLength, pattern.length());
673         final boolean shouldCountBeUpdatedAtLastCharacter =
674                 typeDeclarationToBeMatchedLength > minLength
675                 && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0);
676 
677         int result = 0;
678         for (int idx = 0;
679              idx < minLength
680                 && pattern.charAt(idx) == candidate.charAt(idx);
681              idx++) {
682 
683             if (shouldCountBeUpdatedAtLastCharacter
684                     || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) {
685                 result = idx;
686             }
687         }
688         return result;
689     }
690 
691     /**
692      * Get qualified type declaration name from type ast.
693      *
694      * @param typeDeclAst type declaration ast
695      * @return qualified name of type declaration
696      */
697     private String getQualifiedTypeDeclarationName(DetailAST typeDeclAst) {
698         final String className = typeDeclAst.findFirstToken(TokenTypes.IDENT).getText();
699         String outerClassQualifiedName = null;
700         if (!typeDeclarations.isEmpty()) {
701             outerClassQualifiedName = typeDeclarations.peek().getQualifiedName();
702         }
703         return CheckUtil
704             .getQualifiedTypeDeclarationName(packageName, outerClassQualifiedName, className);
705     }
706 
707     /**
708      * Iterate over all the ast nodes present under {@code ast}.
709      *
710      * @param ast ast
711      * @param variablesStack stack of all the relevant variables in the scope
712      */
713     private void iterateOverBlockContainingLocalAnonInnerClass(
714             DetailAST ast, Deque<VariableDesc> variablesStack) {
715         DetailAST currNode = ast;
716         while (currNode != null) {
717             customVisitToken(currNode, variablesStack);
718             DetailAST toVisit = currNode.getFirstChild();
719             while (currNode != ast && toVisit == null) {
720                 customLeaveToken(currNode, variablesStack);
721                 toVisit = currNode.getNextSibling();
722                 currNode = currNode.getParent();
723             }
724             currNode = toVisit;
725         }
726     }
727 
728     /**
729      * Visit all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
730      * again.
731      *
732      * @param ast ast
733      * @param variablesStack stack of all the relevant variables in the scope
734      */
735     private void customVisitToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
736         final int type = ast.getType();
737         switch (type) {
738             case TokenTypes.DOT -> visitDotToken(ast, variablesStack);
739 
740             case TokenTypes.VARIABLE_DEF -> addLocalVariables(ast, variablesStack);
741 
742             case TokenTypes.IDENT -> visitIdentToken(ast, variablesStack);
743 
744             case TokenTypes.LITERAL_NEW -> {
745                 if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
746                     final TypeDeclDesc obtainedClass = getSuperClassOfAnonInnerClass(ast);
747                     modifyVariablesStack(obtainedClass, variablesStack, ast);
748                 }
749             }
750 
751             default -> {
752                 // No action needed for other token types
753             }
754         }
755     }
756 
757     /**
758      * Leave all ast nodes under {@link UnusedLocalVariableCheck#anonInnerClassHolders} once
759      * again.
760      *
761      * @param ast ast
762      * @param variablesStack stack of all the relevant variables in the scope
763      */
764     private void customLeaveToken(DetailAST ast, Deque<VariableDesc> variablesStack) {
765         logViolations(ast, variablesStack);
766     }
767 
768     /**
769      * Whether to check identifier token nested under dotAst.
770      *
771      * @param dotAst dotAst
772      * @return true if ident nested under dotAst should be checked
773      */
774     private static boolean shouldCheckIdentTokenNestedUnderDot(DetailAST dotAst) {
775 
776         return TokenUtil.findFirstTokenByPredicate(dotAst,
777                         childAst -> {
778                             return TokenUtil.isOfType(childAst,
779                                     UNACCEPTABLE_CHILD_OF_DOT);
780                         })
781                 .isEmpty();
782     }
783 
784     /**
785      * Checks the identifier ast.
786      *
787      * @param identAst ast of type {@link TokenTypes#IDENT}
788      * @param variablesStack stack of all the relevant variables in the scope
789      */
790     private static void checkIdentifierAst(DetailAST identAst, Deque<VariableDesc> variablesStack) {
791         for (VariableDesc variableDesc : variablesStack) {
792             if (identAst.getText().equals(variableDesc.getName())
793                     && !isLeftHandSideValue(identAst)) {
794                 variableDesc.registerAsUsed();
795                 break;
796             }
797         }
798     }
799 
800     /**
801      * Find the scope of variable.
802      *
803      * @param variableDef ast of type {@link TokenTypes#VARIABLE_DEF}
804      * @return scope of variableDef
805      */
806     private static DetailAST findScopeOfVariable(DetailAST variableDef) {
807         final DetailAST result;
808         final DetailAST parentAst = variableDef.getParent();
809         if (TokenUtil.isOfType(parentAst, TokenTypes.SLIST, TokenTypes.OBJBLOCK)) {
810             result = parentAst;
811         }
812         else {
813             result = parentAst.getParent();
814         }
815         return result;
816     }
817 
818     /**
819      * Checks whether the ast of type {@link TokenTypes#IDENT} is
820      * used as left-hand side value. An identifier is being used as a left-hand side
821      * value if it is used as the left operand of an assignment or as an
822      * operand of a stand-alone increment or decrement.
823      *
824      * @param identAst ast of type {@link TokenTypes#IDENT}
825      * @return true if identAst is used as a left-hand side value
826      */
827     private static boolean isLeftHandSideValue(DetailAST identAst) {
828         final DetailAST parent = identAst.getParent();
829         return isStandAloneIncrementOrDecrement(identAst)
830                 || parent.getType() == TokenTypes.ASSIGN
831                 && identAst != parent.getLastChild();
832     }
833 
834     /**
835      * Checks whether the ast of type {@link TokenTypes#IDENT} is used as
836      * an operand of a stand-alone increment or decrement.
837      *
838      * @param identAst ast of type {@link TokenTypes#IDENT}
839      * @return true if identAst is used as an operand of stand-alone
840      *         increment or decrement
841      */
842     private static boolean isStandAloneIncrementOrDecrement(DetailAST identAst) {
843         final DetailAST parent = identAst.getParent();
844         final DetailAST grandParent = parent.getParent();
845         return TokenUtil.isOfType(parent, INCREMENT_AND_DECREMENT_TOKENS)
846                 && TokenUtil.isOfType(grandParent, TokenTypes.EXPR)
847                 && !isIncrementOrDecrementVariableUsed(grandParent);
848     }
849 
850     /**
851      * A variable with increment or decrement operator is considered used if it
852      * is used as an argument or as an array index or for assigning value
853      * to a variable.
854      *
855      * @param exprAst ast of type {@link TokenTypes#EXPR}
856      * @return true if variable nested in exprAst is used
857      */
858     private static boolean isIncrementOrDecrementVariableUsed(DetailAST exprAst) {
859         return TokenUtil.isOfType(exprAst.getParent(), INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES)
860                 && exprAst.getParent().getParent().getType() != TokenTypes.FOR_ITERATOR;
861     }
862 
863     /**
864      * Maintains information about the variable.
865      */
866     private static final class VariableDesc {
867 
868         /**
869          * The name of the variable.
870          */
871         private final String name;
872 
873         /**
874          * Ast of type {@link TokenTypes#TYPE}.
875          */
876         private final DetailAST typeAst;
877 
878         /**
879          * The scope of variable is determined by the ast of type
880          * {@link TokenTypes#SLIST} or {@link TokenTypes#LITERAL_FOR}
881          * or {@link TokenTypes#OBJBLOCK} which is enclosing the variable.
882          */
883         private final DetailAST scope;
884 
885         /**
886          * Is an instance variable or a class variable.
887          */
888         private boolean instVarOrClassVar;
889 
890         /**
891          * Is the variable used.
892          */
893         private boolean used;
894 
895         /**
896          * Create a new VariableDesc instance.
897          *
898          * @param name name of the variable
899          * @param typeAst ast of type {@link TokenTypes#TYPE}
900          * @param scope ast of type {@link TokenTypes#SLIST} or
901          *              {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
902          *              which is enclosing the variable
903          */
904         private VariableDesc(String name, DetailAST typeAst, DetailAST scope) {
905             this.name = name;
906             this.typeAst = typeAst;
907             this.scope = scope;
908         }
909 
910         /**
911          * Create a new VariableDesc instance.
912          *
913          * @param name name of the variable
914          */
915         private VariableDesc(String name) {
916             this(name, null, null);
917         }
918 
919         /**
920          * Create a new VariableDesc instance.
921          *
922          * @param name name of the variable
923          * @param scope ast of type {@link TokenTypes#SLIST} or
924          *              {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
925          *              which is enclosing the variable
926          */
927         private VariableDesc(String name, DetailAST scope) {
928             this(name, null, scope);
929         }
930 
931         /**
932          * Get the name of variable.
933          *
934          * @return name of variable
935          */
936         /* package */ String getName() {
937             return name;
938         }
939 
940         /**
941          * Get the associated ast node of type {@link TokenTypes#TYPE}.
942          *
943          * @return the associated ast node of type {@link TokenTypes#TYPE}
944          */
945         /* package */ DetailAST getTypeAst() {
946             return typeAst;
947         }
948 
949         /**
950          * Get ast of type {@link TokenTypes#SLIST}
951          * or {@link TokenTypes#LITERAL_FOR} or {@link TokenTypes#OBJBLOCK}
952          * which is enclosing the variable i.e. its scope.
953          *
954          * @return the scope associated with the variable
955          */
956         /* package */ DetailAST getScope() {
957             return scope;
958         }
959 
960         /**
961          * Register the variable as used.
962          */
963         /* package */ void registerAsUsed() {
964             used = true;
965         }
966 
967         /**
968          * Register the variable as an instance variable or
969          * class variable.
970          */
971         /* package */ void registerAsInstOrClassVar() {
972             instVarOrClassVar = true;
973         }
974 
975         /**
976          * Is the variable used or not.
977          *
978          * @return true if variable is used
979          */
980         /* package */ boolean isUsed() {
981             return used;
982         }
983 
984         /**
985          * Is an instance variable or a class variable.
986          *
987          * @return true if is an instance variable or a class variable
988          */
989         /* package */ boolean isInstVarOrClassVar() {
990             return instVarOrClassVar;
991         }
992     }
993 
994     /**
995      * Maintains information about the type declaration.
996      * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF}
997      * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF}
998      * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration.
999      */
1000     private static final class TypeDeclDesc {
1001 
1002         /**
1003          * Complete type declaration name with package name and outer type declaration name.
1004          */
1005         private final String qualifiedName;
1006 
1007         /**
1008          * Depth of nesting of type declaration.
1009          */
1010         private final int depth;
1011 
1012         /**
1013          * Type declaration ast node.
1014          */
1015         private final DetailAST typeDeclAst;
1016 
1017         /**
1018          * A stack of type declaration's instance and static variables.
1019          */
1020         private final Deque<VariableDesc> instanceAndClassVarStack;
1021 
1022         /**
1023          * Create a new TypeDeclDesc instance.
1024          *
1025          * @param qualifiedName qualified name
1026          * @param depth depth of nesting
1027          * @param typeDeclAst type declaration ast node
1028          */
1029         private TypeDeclDesc(String qualifiedName, int depth,
1030                 DetailAST typeDeclAst) {
1031             this.qualifiedName = qualifiedName;
1032             this.depth = depth;
1033             this.typeDeclAst = typeDeclAst;
1034             instanceAndClassVarStack = new ArrayDeque<>();
1035         }
1036 
1037         /**
1038          * Get the complete type declaration name i.e. type declaration name with package name
1039          * and outer type declaration name.
1040          *
1041          * @return qualified class name
1042          */
1043         /* package */ String getQualifiedName() {
1044             return qualifiedName;
1045         }
1046 
1047         /**
1048          * Get the depth of type declaration.
1049          *
1050          * @return the depth of nesting of type declaration
1051          */
1052         /* package */ int getDepth() {
1053             return depth;
1054         }
1055 
1056         /**
1057          * Get the type declaration ast node.
1058          *
1059          * @return ast node of the type declaration
1060          */
1061         /* package */ DetailAST getTypeDeclAst() {
1062             return typeDeclAst;
1063         }
1064 
1065         /**
1066          * Get the copy of variables in instanceAndClassVar stack with updated scope.
1067          *
1068          * @param literalNewAst ast node of type {@link TokenTypes#LITERAL_NEW}
1069          * @return copy of variables in instanceAndClassVar stack with updated scope.
1070          */
1071         /* package */ Deque<VariableDesc> getUpdatedCopyOfVarStack(DetailAST literalNewAst) {
1072             final DetailAST updatedScope = literalNewAst;
1073             final Deque<VariableDesc> instAndClassVarDeque = new ArrayDeque<>();
1074             instanceAndClassVarStack.forEach(instVar -> {
1075                 final VariableDesc variableDesc = new VariableDesc(instVar.getName(),
1076                         updatedScope);
1077                 variableDesc.registerAsInstOrClassVar();
1078                 instAndClassVarDeque.push(variableDesc);
1079             });
1080             return instAndClassVarDeque;
1081         }
1082 
1083         /**
1084          * Add an instance variable or class variable to the stack.
1085          *
1086          * @param variableDesc variable to be added
1087          */
1088         /* package */ void addInstOrClassVar(VariableDesc variableDesc) {
1089             instanceAndClassVarStack.push(variableDesc);
1090         }
1091     }
1092 }