Repository: checkstyle/checkstyle (Java)

Issue:
UnusedLocalVariable: False Positive when  inner class has same field as variable 
I have read check documentation: https://checkstyle.sourceforge.io/config_coding.html#UnusedLocalVariable
I have downloaded the latest checkstyle from: https://checkstyle.org/cmdline.html#Download_and_Run
I have executed the cli and showed it below, as cli describes the problem better than 1,000 words

```bash
/var/tmp $ javac Test.java

/var/tmp $ cat config.xml
```
```xml
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
        "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
        "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
    <module name="TreeWalker">
        <module name="UnusedLocalVariable"/>
    </module>
</module>
```
```
/var/tmp $ cat Test.java
```
```java
public class Test {

    static class m {
        static class h {
            public int a = 12;
        }
    }

    static class j {
        static void method() {
            int a = 1000; // unexpected violation
            m.h obj = new m.h() { // object of Test.j.m.h and not of Test.m.h
                @Override
                void method() {
                    System.out.println("a = " + a); // local var a
                }
            };
            obj.method();
        }

        static class m {
            static class h {
                void method() {
                }
            }
        }
    }

    public static void main(String[] args) {
        j.method();
    }
}
```
```
/var/tmp $ RUN_LOCALE="-Duser.language=en -Duser.country=US"
/var/tmp $ java $RUN_LOCALE -jar checkstyle-10.0-all.jar -c config.xml Test.java
Starting audit...
[ERROR] /var/tmp/Test.java:11:13: Unused local variable 'a'. [UnusedLocalVariable]
Audit done.
Checkstyle ends with 1 errors.
```
```
/var/tmp $ java Test.java
a = 1000
```
---

**Describe what you expect in detail.**
```
/var/tmp $ RUN_LOCALE="-Duser.language=en -Duser.country=US"
/var/tmp $ java $RUN_LOCALE -jar checkstyle-10.0-all.jar -c config.xml Test.java
Starting audit...
Audit done.
```

---

Discovered while working on #11365, the source has been identified, the fix has been done locally, will be pushed soon.


Discussion:
@Vishakhakumawat Fix has already been done, just pushing to remote is remaining, it is mentioned in issue description 
Closed via #15969 



Patch:
diff --git a/src/main/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheck.java b/src/main/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheck.java
index 9df307e2491..a04be97f7fb 100644
--- a/src/main/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheck.java
+++ b/src/main/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheck.java
@@ -562,7 +562,7 @@ private TypeDeclDesc getSuperClassOfAnonInnerClass(DetailAST literalNewAst) {
         else {
             final List<TypeDeclDesc> typeDeclWithSameName = typeDeclWithSameName(shortNameOfClass);
             if (!typeDeclWithSameName.isEmpty()) {
-                obtainedClass = getTheNearestClass(
+                obtainedClass = getClosestMatchingTypeDeclaration(
                         anonInnerAstToTypeDeclDesc.get(literalNewAst).getQualifiedName(),
                         typeDeclWithSameName);
             }
@@ -631,10 +631,10 @@ private boolean hasSameNameAsSuperClass(String superClassName, TypeDeclDesc type
      * @param typeDeclWithSameName typeDeclarations which have the same name as the super class
      * @return the nearest class
      */
-    private static TypeDeclDesc getTheNearestClass(String outerTypeDeclName,
+    private static TypeDeclDesc getClosestMatchingTypeDeclaration(String outerTypeDeclName,
             List<TypeDeclDesc> typeDeclWithSameName) {
         return Collections.min(typeDeclWithSameName, (first, second) -> {
-            return getTypeDeclarationNameMatchingCountDiff(outerTypeDeclName, first, second);
+            return calculateTypeDeclarationDistance(outerTypeDeclName, first, second);
         });
     }
 
@@ -642,23 +642,69 @@ private static TypeDeclDesc getTheNearestClass(String outerTypeDeclName,
      * Get the difference between type declaration name matching count. If the
      * difference between them is zero, then their depth is compared to obtain the result.
      *
-     * @param outerTypeDeclName outer type declaration of anonymous inner class
-     * @param firstTypeDecl first input type declaration
-     * @param secondTypeDecl second input type declaration
+     * @param outerTypeName outer type declaration of anonymous inner class
+     * @param firstType first input type declaration
+     * @param secondType second input type declaration
      * @return difference between type declaration name matching count
      */
-    private static int getTypeDeclarationNameMatchingCountDiff(String outerTypeDeclName,
-                                                               TypeDeclDesc firstTypeDecl,
-                                                               TypeDeclDesc secondTypeDecl) {
-        int diff = Integer.compare(
-            CheckUtil.typeDeclarationNameMatchingCount(
-                outerTypeDeclName, secondTypeDecl.getQualifiedName()),
-            CheckUtil.typeDeclarationNameMatchingCount(
-                outerTypeDeclName, firstTypeDecl.getQualifiedName()));
-        if (diff == 0) {
-            diff = Integer.compare(firstTypeDecl.getDepth(), secondTypeDecl.getDepth());
+    private static int calculateTypeDeclarationDistance(String outerTypeName,
+                                                        TypeDeclDesc firstType,
+                                                        TypeDeclDesc secondType) {
+        final int firstMatchCount =
+                countMatchingQualifierChars(outerTypeName, firstType.getQualifiedName());
+        final int secondMatchCount =
+                countMatchingQualifierChars(outerTypeName, secondType.getQualifiedName());
+        final int matchDistance = Integer.compare(secondMatchCount, firstMatchCount);
+
+        final int distance;
+        if (matchDistance == 0) {
+            distance = Integer.compare(firstType.getDepth(), secondType.getDepth());
+        }
+        else {
+            distance = matchDistance;
         }
-        return diff;
+
+        return distance;
+    }
+
+    /**
+     * Calculates the type declaration matching count for the superclass of an anonymous inner
+     * class.
+     *
+     * <p>
+     * For example, if the pattern class is {@code Main.ClassOne} and the class to be matched is
+     * {@code Main.ClassOne.ClassTwo.ClassThree}, then the matching count would be calculated by
+     * comparing the characters at each position, and updating the count whenever a '.'
+     * is encountered.
+     * This is necessary because pattern class can include anonymous inner classes, unlike regular
+     * inheritance where nested classes cannot be extended.
+     * </p>
+     *
+     * @param pattern type declaration to match against
+     * @param candidate type declaration to be matched
+     * @return the type declaration matching count
+     */
+    private static int countMatchingQualifierChars(String pattern,
+                                                   String candidate) {
+        final int typeDeclarationToBeMatchedLength = candidate.length();
+        final int minLength = Math
+                .min(typeDeclarationToBeMatchedLength, pattern.length());
+        final boolean shouldCountBeUpdatedAtLastCharacter =
+                typeDeclarationToBeMatchedLength > minLength
+                && candidate.charAt(minLength) == PACKAGE_SEPARATOR.charAt(0);
+
+        int result = 0;
+        for (int idx = 0;
+             idx < minLength
+                && pattern.charAt(idx) == candidate.charAt(idx);
+             idx++) {
+
+            if (shouldCountBeUpdatedAtLastCharacter
+                    || pattern.charAt(idx) == PACKAGE_SEPARATOR.charAt(0)) {
+                result = idx;
+            }
+        }
+        return result;
     }
 
     /**


Test patch:
diff --git a/src/test/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheckTest.java b/src/test/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheckTest.java
index f3eeec2b4e6..31a4f9f2b2f 100644
--- a/src/test/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheckTest.java
+++ b/src/test/java/com/puppycrawl/tools/checkstyle/checks/coding/UnusedLocalVariableCheckTest.java
@@ -557,4 +557,26 @@ public void testClearStatePackageDef() throws Exception {
                         Objects::isNull))
                 .isTrue();
     }
+
+    @Test
+    public void testUnusedLocalVarInAnonInnerClasses2() throws Exception {
+        final String[] expected = {
+            "20:13: " + getCheckMessage(MSG_UNUSED_LOCAL_VARIABLE, "s"),
+        };
+        verifyWithInlineConfigParser(
+                getPath("InputUnusedLocalVariableAnonInnerClasses2.java"),
+                expected);
+    }
+
+    @Test
+    public void testUnusedLocalVarInAnonInnerClasses3() throws Exception {
+        final String[] expected = {
+            "13:13: " + getCheckMessage(MSG_UNUSED_LOCAL_VARIABLE, "a"),
+            "20:13: " + getCheckMessage(MSG_UNUSED_LOCAL_VARIABLE, "obj"),
+            "32:13: " + getCheckMessage(MSG_UNUSED_LOCAL_VARIABLE, "s"),
+        };
+        verifyWithInlineConfigParser(
+                getPath("InputUnusedLocalVariableAnonInnerClasses3.java"),
+                expected);
+    }
 }
diff --git a/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses2.java b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses2.java
new file mode 100644
index 00000000000..089be4d09d1
--- /dev/null
+++ b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses2.java
@@ -0,0 +1,55 @@
+/*
+UnusedLocalVariable
+allowUnnamedVariables = false
+
+*/
+
+package com.puppycrawl.tools.checkstyle.checks.coding.unusedlocalvariable;
+
+public class InputUnusedLocalVariableAnonInnerClasses2 {
+
+    static class m {
+        static class h {
+            public int a = 12;
+        }
+    }
+
+    static class j {
+        static void method() {
+            int a = 1000;
+            int s = 13; // violation, 'Unused local variable'
+            int q = 14;
+            m.h obj = new m.h() {
+                @Override
+                void method() {
+                    Integer.valueOf(a + s);
+                }
+
+                m.h obj = new m.h() { // ok, anonymous instance field
+                    @Override
+                    void method() {
+                        Integer.valueOf(q);
+                    }
+                };
+
+            };
+            obj.method();
+        }
+
+        static class m {
+            static class h {
+                int s = 12;
+                void method() {
+                }
+            }
+        }
+    }
+
+    static class jasper {
+        static class m {
+            static class h {
+                int q = 12;
+            }
+        }
+    }
+}
diff --git a/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses3.java b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses3.java
new file mode 100644
index 00000000000..52594db2ba6
--- /dev/null
+++ b/src/test/resources/com/puppycrawl/tools/checkstyle/checks/coding/unusedlocalvariable/InputUnusedLocalVariableAnonInnerClasses3.java
@@ -0,0 +1,64 @@
+/*
+UnusedLocalVariable
+allowUnnamedVariables = false
+
+*/
+
+package com.puppycrawl.tools.checkstyle.checks.coding.unusedlocalvariable;
+
+public class InputUnusedLocalVariableAnonInnerClasses3 {
+
+    static class m {
+        void method() {
+            int a = 12; // violation, 'Unused local variable'
+        }
+    }
+
+    static class myClass {
+        static void method() {
+            int a = 12;
+            m obj = new m() { // violation, 'Unused local variable'
+                @Override
+                void method() {
+                    Integer.valueOf(a);
+                }
+            };
+        }
+    }
+
+    static class j {
+        static void method() {
+            int a = 1000;
+            int s = 13; // violation, 'Unused local variable'
+            int q = 14;
+            m obj = new m() {
+                @Override
+                void method() {
+                    Integer.valueOf(a + s);
+                }
+
+                m obj = new m() { // ok, anonymous instance field
+                    @Override
+                    void method() {
+                        Integer.valueOf(q);
+                    }
+                };
+
+            };
+            obj.method();
+        }
+
+        static class m {
+            int s = 12;
+
+            void method() {
+            }
+        }
+    }
+
+    static class jasper {
+        static class m {
+            int q = 12;
+        }
+    }
+}