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