001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayDeque;
023import java.util.Collections;
024import java.util.Deque;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.Set;
033
034import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
035import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
036import com.puppycrawl.tools.checkstyle.api.DetailAST;
037import com.puppycrawl.tools.checkstyle.api.TokenTypes;
038import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
039import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
040import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
041
042/**
043 * <div>
044 * Checks that a local variable is declared and/or assigned, but not used.
045 * Supports
046 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.30">
047 * pattern variables</a>.
048 * Doesn't check
049 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.12.3">
050 * array components</a> as array
051 * components are classified as different kind of variables by
052 * <a href="https://docs.oracle.com/javase/specs/jls/se17/html/index.html">JLS</a>.
053 * </div>
054 *
055 * @since 9.3
056 */
057@FileStatefulCheck
058public class UnusedLocalVariableCheck extends AbstractCheck {
059
060    /**
061     * A key is pointing to the warning message text in "messages.properties"
062     * file.
063     */
064    public static final String MSG_UNUSED_LOCAL_VARIABLE = "unused.local.var";
065
066    /**
067     * A key is pointing to the warning message text in "messages.properties"
068     * file.
069     */
070    public static final String MSG_UNUSED_NAMED_LOCAL_VARIABLE = "unused.named.local.var";
071
072    /**
073     * An array of increment and decrement tokens.
074     */
075    private static final int[] INCREMENT_AND_DECREMENT_TOKENS = {
076        TokenTypes.POST_INC,
077        TokenTypes.POST_DEC,
078        TokenTypes.INC,
079        TokenTypes.DEC,
080    };
081
082    /**
083     * An array of scope tokens.
084     */
085    private static final int[] SCOPES = {
086        TokenTypes.SLIST,
087        TokenTypes.LITERAL_FOR,
088        TokenTypes.OBJBLOCK,
089    };
090
091    /**
092     * An array of unacceptable children of ast of type {@link TokenTypes#DOT}.
093     */
094    private static final int[] UNACCEPTABLE_CHILD_OF_DOT = {
095        TokenTypes.DOT,
096        TokenTypes.METHOD_CALL,
097        TokenTypes.LITERAL_NEW,
098        TokenTypes.LITERAL_SUPER,
099        TokenTypes.LITERAL_CLASS,
100        TokenTypes.LITERAL_THIS,
101    };
102
103    /**
104     * An array of unacceptable parent of ast of type {@link TokenTypes#IDENT}.
105     */
106    private static final int[] UNACCEPTABLE_PARENT_OF_IDENT = {
107        TokenTypes.VARIABLE_DEF,
108        TokenTypes.DOT,
109        TokenTypes.LITERAL_NEW,
110        TokenTypes.PATTERN_VARIABLE_DEF,
111        TokenTypes.METHOD_CALL,
112        TokenTypes.TYPE,
113    };
114
115    /**
116     * An array of blocks in which local anon inner classes can exist.
117     */
118    private static final int[] ANONYMOUS_CLASS_PARENT_TOKENS = {
119        TokenTypes.METHOD_DEF,
120        TokenTypes.CTOR_DEF,
121        TokenTypes.STATIC_INIT,
122        TokenTypes.INSTANCE_INIT,
123        TokenTypes.COMPACT_CTOR_DEF,
124    };
125
126    /**
127     * An array of token types that indicate a variable is being used within
128     * an expression involving increment or decrement operators, or within a switch statement.
129     * When a token of one of these types is the parent of an expression, it indicates that the
130     * variable associated with the increment or decrement operation is being used.
131     * Ex:- TokenTypes.LITERAL_SWITCH: Indicates a switch statement. Variables used within the
132     * switch expression are considered to be used
133     */
134    private static final int[] INCREMENT_DECREMENT_VARIABLE_USAGE_TYPES = {
135        TokenTypes.ELIST,
136        TokenTypes.INDEX_OP,
137        TokenTypes.ASSIGN,
138        TokenTypes.LITERAL_SWITCH,
139    };
140
141    /** Package separator. */
142    private static final String PACKAGE_SEPARATOR = ".";
143
144    /**
145     *  Symbol used to represent unnamed variables in Java pattern matching.
146     */
147    private static final String UNNAMED_VAR = "_";
148
149    /**
150     * Constant for JDK 22 version number.
151     */
152    private static final int JDK_22 = 22;
153
154    /**
155     * Keeps tracks of the variables declared in file.
156     */
157    private final Deque<VariableDesc> variables = new ArrayDeque<>();
158
159    /**
160     * Keeps track of all the type declarations present in the file.
161     * Pops the type out of the stack while leaving the type
162     * in visitor pattern.
163     */
164    private final Deque<TypeDeclDesc> typeDeclarations = new ArrayDeque<>();
165
166    /**
167     * Maps type declaration ast to their respective TypeDeclDesc objects.
168     */
169    private final Map<DetailAST, TypeDeclDesc> typeDeclAstToTypeDeclDesc = new LinkedHashMap<>();
170
171    /**
172     * Maps local anonymous inner class to the TypeDeclDesc object
173     * containing it.
174     */
175    private final Map<DetailAST, TypeDeclDesc> anonInnerAstToTypeDeclDesc = new HashMap<>();
176
177    /**
178     * Set of tokens of type {@link UnusedLocalVariableCheck#ANONYMOUS_CLASS_PARENT_TOKENS}
179     * and {@link TokenTypes#LAMBDA} in some cases.
180     */
181    private final Set<DetailAST> anonInnerClassHolders = new HashSet<>();
182
183    /**
184     * Allow variables named with a single underscore
185     * (known as  <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
186     *  unnamed variables</a> in Java 21+).
187     */
188    private boolean allowUnnamedVariables = true;
189
190    /**
191     * Set the JDK version that you are using.
192     * Old JDK version numbering is supported (e.g. 1.8 for Java 8)
193     * as well as just the major JDK version alone (e.g. 8) is supported.
194     * This property only considers features from officially released
195     * Java versions as supported. Features introduced in preview releases
196     * are not considered supported until they are included in a non-preview release.
197     * Before JDK 22, named pattern variables in switch labels and instanceof
198     * record Destructuring cannot be replaced with {@code _}, so violations
199     * on them are suppressed when jdkVersion is set below 22.
200     */
201    private int jdkVersion = JDK_22;
202
203    /**
204     * Name of the package.
205     */
206    private String packageName;
207
208    /**
209     * Depth at which a type declaration is nested, 0 for top level type declarations.
210     */
211    private int depth;
212
213    /**
214     * Creates a new {@code UnusedLocalVariableCheck} instance.
215     */
216    public UnusedLocalVariableCheck() {
217        // no code by default
218    }
219
220    /**
221     * Setter to allow variables named with a single underscore
222     * (known as <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
223     * unnamed variables</a> in Java 21+).
224     *
225     * @param allowUnnamedVariables true or false.
226     * @since 10.18.0
227     */
228    public void setAllowUnnamedVariables(boolean allowUnnamedVariables) {
229        this.allowUnnamedVariables = allowUnnamedVariables;
230    }
231
232    /**
233     * Setter to set the JDK version that you are using.
234     * Old JDK version numbering is supported (e.g. 1.8 for Java 8)
235     * as well as just the major JDK version alone (e.g. 8) is supported.
236     * This property only considers features from officially released
237     * Java versions as supported. Features introduced in preview releases
238     * are not considered supported until they are included in a non-preview release.
239     * Before JDK 22, named pattern variables in switch labels and instanceof
240     * record Destructuring cannot be replaced with {@code _}, so violations
241     * on them are suppressed when jdkVersion is set below 22.
242     *
243     * @param jdkVersion the Java version.
244     * @since 13.7.0
245     */
246    public void setJdkVersion(String jdkVersion) {
247        final String singleVersionNumber;
248        if (jdkVersion.startsWith("1.")) {
249            singleVersionNumber = jdkVersion.substring(2);
250        }
251        else {
252            singleVersionNumber = jdkVersion;
253        }
254        this.jdkVersion = Integer.parseInt(singleVersionNumber);
255    }
256
257    @Override
258    public int[] getDefaultTokens() {
259        return new int[] {
260            TokenTypes.DOT,
261            TokenTypes.VARIABLE_DEF,
262            TokenTypes.IDENT,
263            TokenTypes.SLIST,
264            TokenTypes.LITERAL_FOR,
265            TokenTypes.OBJBLOCK,
266            TokenTypes.CLASS_DEF,
267            TokenTypes.INTERFACE_DEF,
268            TokenTypes.ANNOTATION_DEF,
269            TokenTypes.PACKAGE_DEF,
270            TokenTypes.LITERAL_NEW,
271            TokenTypes.METHOD_DEF,
272            TokenTypes.CTOR_DEF,
273            TokenTypes.STATIC_INIT,
274            TokenTypes.INSTANCE_INIT,
275            TokenTypes.COMPILATION_UNIT,
276            TokenTypes.LAMBDA,
277            TokenTypes.ENUM_DEF,
278            TokenTypes.RECORD_DEF,
279            TokenTypes.COMPACT_CTOR_DEF,
280            TokenTypes.PATTERN_VARIABLE_DEF,
281        };
282    }
283
284    @Override
285    public int[] getAcceptableTokens() {
286        return getDefaultTokens();
287    }
288
289    @Override
290    public int[] getRequiredTokens() {
291        return getDefaultTokens();
292    }
293
294    @Override
295    public void beginTree(DetailAST root) {
296        variables.clear();
297        typeDeclarations.clear();
298        typeDeclAstToTypeDeclDesc.clear();
299        anonInnerAstToTypeDeclDesc.clear();
300        anonInnerClassHolders.clear();
301        packageName = null;
302        depth = 0;
303    }
304
305    @Override
306    public void visitToken(DetailAST ast) {
307        final int type = ast.getType();
308        if (type == TokenTypes.DOT) {
309            visitDotToken(ast, variables);
310        }
311        else if (type == TokenTypes.VARIABLE_DEF && !skipUnnamedVariables(ast)) {
312            visitVariableDefToken(ast);
313        }
314        else if (type == TokenTypes.PATTERN_VARIABLE_DEF
315                && !skipUnnamedPatternVariables(ast)) {
316            addPatternVariable(ast, variables);
317        }
318        else if (type == TokenTypes.IDENT) {
319            visitIdentToken(ast, variables);
320        }
321        else if (isInsideLocalAnonInnerClass(ast)) {
322            visitLocalAnonInnerClass(ast);
323        }
324        else if (isNonLocalTypeDeclaration(ast)) {
325            visitNonLocalTypeDeclarationToken(ast);
326        }
327        else if (type == TokenTypes.PACKAGE_DEF) {
328            packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling());
329        }
330    }
331
332    @Override
333    public void leaveToken(DetailAST ast) {
334        if (TokenUtil.isOfType(ast, SCOPES)) {
335            logViolations(ast, variables);
336        }
337        else if (ast.getType() == TokenTypes.COMPILATION_UNIT) {
338            leaveCompilationUnit();
339        }
340        else if (isNonLocalTypeDeclaration(ast)) {
341            depth--;
342            typeDeclarations.pop();
343        }
344    }
345
346    /**
347     * Visit ast of type {@link TokenTypes#DOT}.
348     *
349     * @param dotAst dotAst
350     * @param variablesStack stack of all the relevant variables in the scope
351     */
352    private static void visitDotToken(DetailAST dotAst, Deque<VariableDesc> variablesStack) {
353        if (dotAst.getParent().getType() != TokenTypes.LITERAL_NEW
354                && shouldCheckIdentTokenNestedUnderDot(dotAst)) {
355            final DetailAST identifier = dotAst.findFirstToken(TokenTypes.IDENT);
356            if (identifier != null) {
357                checkIdentifierAst(identifier, variablesStack);
358            }
359        }
360    }
361
362    /**
363     * Visit ast of type {@link TokenTypes#VARIABLE_DEF}.
364     *
365     * @param varDefAst varDefAst
366     */
367    private void visitVariableDefToken(DetailAST varDefAst) {
368        addLocalVariables(varDefAst, variables);
369        addInstanceOrClassVar(varDefAst);
370    }
371
372    /**
373     * Visit ast of type {@link TokenTypes#IDENT}.
374     *
375     * @param identAst identAst
376     * @param variablesStack stack of all the relevant variables in the scope
377     */
378    private static void visitIdentToken(DetailAST identAst, Deque<VariableDesc> variablesStack) {
379        final DetailAST parent = identAst.getParent();
380        final boolean isMethodReferenceMethodName = parent.getType() == TokenTypes.METHOD_REF
381                && parent.getFirstChild() != identAst;
382        final boolean isConstructorReference = parent.getType() == TokenTypes.METHOD_REF
383                && parent.getLastChild().getType() == TokenTypes.LITERAL_NEW;
384        final boolean isNestedClassInitialization =
385                TokenUtil.isOfType(identAst.getNextSibling(), TokenTypes.LITERAL_NEW)
386                && parent.getType() == TokenTypes.DOT;
387
388        if (isNestedClassInitialization || !isMethodReferenceMethodName
389                && !isConstructorReference
390                && !TokenUtil.isOfType(parent, UNACCEPTABLE_PARENT_OF_IDENT)) {
391            checkIdentifierAst(identAst, variablesStack);
392        }
393    }
394
395    /**
396     * Visit the non-local type declaration token.
397     *
398     * @param typeDeclAst type declaration ast
399     */
400    private void visitNonLocalTypeDeclarationToken(DetailAST typeDeclAst) {
401        final String qualifiedName = getQualifiedTypeDeclarationName(typeDeclAst);
402        final TypeDeclDesc currTypeDecl = new TypeDeclDesc(qualifiedName, depth, typeDeclAst);
403        depth++;
404        typeDeclarations.push(currTypeDecl);
405        typeDeclAstToTypeDeclDesc.put(typeDeclAst, currTypeDecl);
406    }
407
408    /**
409     * Visit the local anon inner class.
410     *
411     * @param literalNewAst literalNewAst
412     */
413    private void visitLocalAnonInnerClass(DetailAST literalNewAst) {
414        anonInnerAstToTypeDeclDesc.put(literalNewAst, typeDeclarations.peek());
415        anonInnerClassHolders.add(getBlockContainingLocalAnonInnerClass(literalNewAst));
416    }
417
418    /**
419     * Check for skip current {@link TokenTypes#VARIABLE_DEF}
420     * due to <b>allowUnnamedVariable</b> option.
421     *
422     * @param varDefAst varDefAst variable to check
423     * @return true if the current variable should be skipped.
424     */
425    private boolean skipUnnamedVariables(DetailAST varDefAst) {
426        final DetailAST ident = varDefAst.findFirstToken(TokenTypes.IDENT);
427        return allowUnnamedVariables && UNNAMED_VAR.equals(ident.getText());
428    }
429
430    /**
431     * Checks whether the specified current pattern variable is an unnamed pattern variable.
432     *
433     * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
434     * @return true if the current pattern variable should be skipped.
435     */
436    private static boolean skipUnnamedPatternVariables(DetailAST patternVarDefAst) {
437        final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT);
438        return UNNAMED_VAR.equals(ident.getText());
439    }
440
441    /**
442     * Add a pattern variable to the {@code variablesStack} stack.
443     *
444     * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
445     * @param variablesStack stack of all the relevant variables in the scope
446     */
447    private static void addPatternVariable(DetailAST patternVarDefAst,
448            Deque<VariableDesc> variablesStack) {
449        final DetailAST ident = patternVarDefAst.findFirstToken(TokenTypes.IDENT);
450        final DetailAST scope = findScopeOfPatternVariable(patternVarDefAst);
451        final VariableDesc desc = new VariableDesc(ident.getText(), ident, scope);
452        if (isForcedNamePatternVariable(patternVarDefAst)) {
453            desc.registerAsNamedPatternVar();
454        }
455        variablesStack.push(desc);
456    }
457
458    /**
459     * Checks whether the pattern variable is declared in a switch labels and instanceof.
460     *
461     * @param patternVarDefAst ast of type {@link TokenTypes#PATTERN_VARIABLE_DEF}
462     * @return true if the pattern variable is in a forced-name context
463     */
464    private static boolean isForcedNamePatternVariable(DetailAST patternVarDefAst) {
465        return patternVarDefAst.getParent().getType() != TokenTypes.LITERAL_INSTANCEOF;
466    }
467
468    /**
469     * Find the scope of a pattern variable.
470     *
471     * @param patternVarDefAst ast of type.
472     * @return the outermost enclosing {@link TokenTypes#SLIST}, or {@code null} if none.
473     */
474    private static DetailAST findScopeOfPatternVariable(DetailAST patternVarDefAst) {
475        final Deque<DetailAST> slistAncestors = new ArrayDeque<>();
476        for (DetailAST current = patternVarDefAst;
477             current != null;
478             current = current.getParent()) {
479            if (current.getType() == TokenTypes.SLIST) {
480                slistAncestors.push(current);
481            }
482        }
483        return slistAncestors.peekLast();
484    }
485
486    /**
487     * Whether ast node of type {@link TokenTypes#LITERAL_NEW} is a part of a local
488     * anonymous inner class.
489     *
490     * @param literalNewAst ast node of type {@link 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 {@link 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 {@link 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 {@link 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 {@link 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}