Skip to content

Issue #19717: Add input files for FinalLocalVariableCheck about initialization in adjacent catches#19728

Merged
romani merged 1 commit into
checkstyle:masterfrom
lithops-zty:issue-19717-fix-final-local-variable-adjacent-catches
May 27, 2026
Merged

Issue #19717: Add input files for FinalLocalVariableCheck about initialization in adjacent catches#19728
romani merged 1 commit into
checkstyle:masterfrom
lithops-zty:issue-19717-fix-final-local-variable-adjacent-catches

Conversation

@lithops-zty

@lithops-zty lithops-zty commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

fixes: #19717

Fix false negative in FinalLocalVariableCheck for multi-catch blocks.

Problem

When multiple catch blocks each assign the same uninitialized variable, the variable should be reported as a candidate for final because only one catch branch can execute, so the variable is effectively assigned exactly once on every path. Currently, the check silently misses this case.

Root Cause

Two methods in FinalLocalVariableCheck lack awareness of LITERAL_CATCH as a mutually-exclusive branch (analogous to LITERAL_ELSE for if-else and SWITCH_RULE for arrow-switch):

  1. determineAssignmentConditions: its blockTypes array does not include LITERAL_CATCH, so when the second catch block's assignment is processed and candidate.assigned is already true, the check unconditionally sets candidate.alreadyAssigned = true and removes the variable from the final-candidate list.
  2. shouldUpdateUninitializedVariables: when leaving the first catch block's SLIST, the method does not recognize that another catch follows, so updateAllUninitializedVariables() is never called and the variable's uninitialized state is lost before the second catch is visited.

Fix

  1. Add TokenTypes.LITERAL_CATCH to the blockTypes array in determineAssignmentConditions.
  2. Add a new helper isCatchTokenWithAnotherCatchFollowing (symmetric with the existing isCaseTokenWithAnotherCaseFollowing) and call it from shouldUpdateUninitializedVariables:

@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch from a5e4bb5 to 9351b67 Compare April 21, 2026 08:34
@lithops-zty

lithops-zty commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

Github, generate report for FinalLocalVariableCheck/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch from 9351b67 to 7f85265 Compare April 21, 2026 10:34
Comment thread config/checkstyle-checks.xml Outdated
<module name="LambdaBodyLength"/>
<module name="MethodCount">
<property name="maxTotal" value="34"/>
<property name="maxTotal" value="35"/>

@lithops-zty lithops-zty Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value 34 was chosen exactly because this check had 34 methods and was the largest of all checks: (#4539 (comment))
I think it is reasonable to increase it as the updated version now has 35 methods.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can put permanent suppression on this Check + File to our suppression xml, there are already bunch like this.
We do not know how to design classes to be less big and same time easy to read and keep logic very close.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. I restored to 34 and added suppression

@lithops-zty

Copy link
Copy Markdown
Contributor Author

@romani PR ready for review.

@romani romani left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks a lot, minor:

public class InputFinalLocalVariableMultiCatch {
public void demo() throws Throwable {
// violation below, "Variable 'a' should be declared final"
int a; // assigned in adjacent catches

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assigned in adjacent catches

assigned in nested catches


very hard to read such Inputs, yes, it is very comprehensive and compact, but it will not make it good in long run.

lets make them compact in formatting, but keep single variable to validate and right under correct form:

       int u;  // violation
        try { } catch (IndexOutOfBoundsException e) { u = 9; } 

       final int u;
        try { } catch (IndexOutOfBoundsException e) { u = 9; } 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. I've completely separated each case into different try-catch blocks and added the correct version right under each violation.

@romani

romani commented May 11, 2026

Copy link
Copy Markdown
Member

Github, generate report for FinalLocalVariableCheck/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

Failed to download or process the specified configuration(s).
Error details:

Please ensure you've used one of the following commands correctly:

@romani

romani commented May 11, 2026

Copy link
Copy Markdown
Member

Github, generate report for FinalLocalVariable/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

@romani

romani commented May 12, 2026

Copy link
Copy Markdown
Member

@lithops-zty , small update please and we will merge

@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch 2 times, most recently from 7d6ce09 to 9eb5120 Compare May 12, 2026 13:57

@romani romani left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear why some violations are missed


byte b;
try {
b = -1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no violation ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case b is assigned in both try and catch.
It is very hard to say whether assignment will happen only once in try, as we cannot determine syntactically if exceptions will be thrown.
In fact, java disallow final in this case:

(base) zty@zty:/mnt/d/IntellijProjects/checkstyle-playground/inputs/final-local-variable$ cat Test.java 
public class Test {
    public void test() {
        final byte b;
        try {
            b = -1;
        } catch (final NumberFormatException e) {
            b = 0;
        } catch (IllegalStateException | NullPointerException e) {
            b = 0;
        }
    }

}(base) zty@zty:/mnt/d/IntellijProjects/checkstyle-playground/inputs/final-local-variable$ javac Test.java 
Test.java:7: error: variable b might already have been assigned
            b = 0;
            ^
Test.java:9: error: variable b might already have been assigned
            b = 0;
            ^
2 errors

@romani romani May 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but why this violation:

        int a; // violation, "Variable 'a' should be declared final"
        try {
        } catch (final NumberFormatException e) {
            a = 0;
        } catch (IllegalStateException | NullPointerException e) {
            a = 1;
        } catch (final IllegalArgumentException e) {
            a = 2;
        }

as if no exception happens, a is not initialized.

if this is defect of compiler, we should not put violation on this, as eventually it will be fixed in compiler.
Can you looks in jdk for defect on this? if not present you can create :)

@lithops-zty lithops-zty May 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this is a defect, but Java allows uninitialized final variable declaration. However, such variables cannot be read later on and will lead to a compiling error:

(base) zty@zty:/mnt/d/IntellijProjects/checkstyle-playground/inputs/final-local-variable$ cat Test.java 
public class Test {
    public void test() {
        final int a;
        try {
        } catch (final NumberFormatException e) {
            a = 0;
        } catch (IllegalStateException | NullPointerException e) {
            a = 1;
        } catch (final IllegalArgumentException e) {
            a = 2;
        }
        int b = a;
    }

}(base) zty@zty:/mnt/d/IntellijProjects/checkstyle-playground/inputs/final-local-variable$ javac Test.java  
Test.java:12: error: variable a might not have been initialized
        int b = a;
                ^
1 error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something tells me that we should not try to validate this at all, to much uncertainty, even compiler is not consistent.

@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch from 9eb5120 to 1b50d59 Compare May 12, 2026 16:40
@romani

romani commented May 13, 2026

Copy link
Copy Markdown
Member

I more and inclined to not do violations in such code.

We would rather do less violations, to not produce false positives. False positives blocks builds and annoy a lot user. Missed violations are ok. Missed violations on crazy edges are ok.

I recommend to remove functional changes and keep all test extension to make it clear we considered it but decided to not do.

If we in agreement, I will change issue title

@lithops-zty

lithops-zty commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for replying late.
I agree this is an edge case that may not warrant strict enforcement.
But do you think there's merit to leave this an option to users, i.e. add a property to trigger it?
That said, I am OK to remove it completely.

@romani

romani commented May 17, 2026

Copy link
Copy Markdown
Member

I don't see value.
Users usually lazy to look at docs for property, they simply disable Check if it misbehaving or blocking them.

Let's in this PR remove all changes in check, and simply merge Inputs, and we can add some comment over variables that until compiler becomes strict, we should not place violation. And we can merge PR.

Can you create issue over java compiler? In worse case they close it with explanation of reason, that is also good to know.

@lithops-zty

lithops-zty commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

No problem, I will remove all functional changes.

However, I don't see a problem with the compiler, could you elaborate on the exact bug you mentioned?

To me, the compiler works as I would expect in terms of allowing and disallowing final:

  1. assignments in both try and catch
final byte b;  // compile error
try {
    b = -1;
} catch (final NumberFormatException e) {
    b = 0;
}

final is forbidden because try and catch are not mutually exclusive and both assignments may execute.
2. assignments in catches only

final int a;
try {
} catch (final NumberFormatException e) {
    a = 0;
} catch (IllegalStateException | NullPointerException e) {
    a = 1;
}

final is allowed because catches are mutually exclusive and at most one assignment may execute.

@romani

romani commented May 18, 2026

Copy link
Copy Markdown
Member

assignments in both try and catc

Yes, we should not make violation on such code.

  1. assignments in catches only

If exception is not happening, what value will have a variable?
And there might be some method in try that throw RuntimeExceptoon , We can guarantee initialization only by catch Throwable. There might be a lot of nuances, but still checkstyle should not violate this. Nobody writes code like this, so it might be purely academic problem :).

@lithops-zty

Copy link
Copy Markdown
Contributor Author

@romani Thanks for the insights. Now I see there is no value checking case 2 since such variable cannot be used at all despite successful compilation. I have pushed a new commit with only input files.

@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch from 1b50d59 to d90977d Compare May 27, 2026 12:35
@lithops-zty lithops-zty changed the title Issue #19717: Fix FinalLocalVariableCheck for initialization in adjacent catches Issue #19717: Add input files for FinalLocalVariableCheck about initialization in adjacent catches May 27, 2026
@lithops-zty lithops-zty force-pushed the issue-19717-fix-final-local-variable-adjacent-catches branch from d90977d to 371009c Compare May 27, 2026 15:03

@romani romani left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot

@romani romani merged commit cca5189 into checkstyle:master May 27, 2026
127 of 128 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False Negative: FinalLocalVariableCheck does not report for variables assigned in adjacent catches

2 participants