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