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