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