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