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