Skip to content

[java] Fix #6495: Consider length/size in local var for ForLoopCanBeForeach#6521

Closed
leemeii wants to merge 1 commit into
pmd:mainfrom
leemeii:main
Closed

[java] Fix #6495: Consider length/size in local var for ForLoopCanBeForeach#6521
leemeii wants to merge 1 commit into
pmd:mainfrom
leemeii:main

Conversation

@leemeii

@leemeii leemeii commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Describe the PR
This PR fixes a false negative in ForLoopCanBeForeach.

Previously, the rule only recognized loop conditions that directly used array.length or collection.size() in the for condition.
It missed equivalent cases where that value was first assigned to a local variable, for example: [int len = arr.length; for (int i = 0; i < len; i++)].

The fix extends condition analysis to resolve local variable references and inspect their initializer.
If the variable is initialized from array.length or collection.size(), the loop is now correctly reported as replaceable by foreach.

Regression tests are added for both:

array length assigned to a local variable
list size assigned to a local variable

Fix #6495

Ready?

  • Added unit tests for fixed bug/feature
  • Passing all unit tests
  • Complete build ./mvnw clean verify passes (checked automatically by github actions)
  • Added (in-code) documentation (if needed)

@adangel adangel changed the title fix(java): fix false negative in ForLoopCanBeForeach when length/size is stored in local variable [java] Fix #6495: Consider length/size in local var for ForLoopCanBeForeach Mar 24, 2026
@pmd-actions-helper

Copy link
Copy Markdown
Contributor

Documentation Preview

Compared to main:
This changeset changes 0 violations,
introduces 31 new violations, 0 new errors and 0 new configuration errors,
removes 0 violations, 0 errors and 0 configuration errors.

Regression Tester Report

(comment created at 2026-03-24 18:34:13+00:00 for 2e69d19)

@adangel adangel 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 for the PR!

I wonder, what happens, if the local variable len would be used after the for loop, e.g.

public class PMD_FN_Demo {
    public void testFalseNegative(long[] counts) {
        double total = 0;
        int len = counts.length;
        for (int i = 0; i < len; i++) {
            total += counts[i];
        }
        System.out.println(len);
    }
}

Should we still report it? ATM moment, I think, yes, we should (the suggested "fix" would be just to replace the for loop and the len variable stays - it could even be used inside the loop).

Now, that we have the regression report (see #6521 (comment) ) could you please have a look at the findings? Are these correct?

The one finding in spring project is difficult: https://github.com/spring-projects/spring-framework/blob/v5.3.13/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java#L219
Changing the for-loop to a for-each-loop would create a "ConcurrentModificationException" at runtime, since the list is modified in the loop. But I think, this is not related to your change, as the the size variable could be inlined, then this false-positive would be there before your change. What do you think? This case looks to me like a different issue?

ASTVariableId varId = sym.tryGetNode();
if (varId != null) {
ASTExpression init = varId.getInitializer();
if (init != null) {

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.

This will not work if the variable has been reassigned to some other value, or if the array or collection variable has been mutated between the time we got its length and the loop... I'm not sure this is a very worthwhile false negative to fix tbh... It would open a very large can of worms.

@zbynek

zbynek commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

The example from Spring shows that this is not applicable if there are any operations in the loop that change the iterable's length (using enhanced for would create an infinite loop or a ConcurrentModificationException):

int size = aliases.size();
for (int j = 0; j < size; j++) {
  List<Method> additional = mapping.aliasedBy.get(aliases.get(j));
  if (additional != null) {
	aliases.addAll(additional);
  }
}

Also what about reassigning?

void printFirst(int count) {
  int toPrint= array.length;
  if (toPrint > count) {
    toPrint = count;
  }
  for (int i=0; i< toPrint; i++) {
    System.out.println(array[i]);
  }
}

@adangel adangel added the needs:user-input Maintainers are waiting for feedback from author label May 7, 2026
@adangel

adangel commented May 7, 2026

Copy link
Copy Markdown
Member

Also what about reassigning?

I think, if we fix this false negative, we should restrict it to a local variable, that is effectively final - means it is initialized with array.length/list.size() and never changed.

This is what the sample code in #6495 shows.

For the concurrent modification exception problem - this might be a false-positive, different issue. If there are more interactions other than list.get(i) or array[i] inside the for-loop, then we should not suggest to use a foreach loop.

Ok, partly - it would be a new false positive (one worm out of the can): Previously, we didn't consider such for loops, because the list.size() was declared in a variable (otherwise they would get a OOM when running the code, as the list is getting bigger and bigger). Now we consider this for loop, and then we should ignore it, because they call list.addAll() (see https://github.com/spring-projects/spring-framework/blob/v5.3.13/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java#L219 ).

@leemeii Could you do the following tasks?

  • Recreated this PR from a separate feature branch and not your main branch, so that all your PRs are isolated
  • Add the following test cases
    <test-code>
        <description>list is modified</description>
        <expected-problems>0</expected-problems>
        <code><![CDATA[
import java.util.List;

class Foo {
    void m(List<String> list) {
        // if this loop would be changed to foreach, it potentially would throw a ConcurrentModificationException
        int size = list.size();
        for (int j = 0; j < size; j++) {
            List<String> additional = getAdditional(list.get(j));
            if (additional != null) {
                list.addAll(additional);
            }
        }
    }

    void m2(List<String> list) {
        // if this loop would be changed to foreach, it potentially would throw a ConcurrentModificationException
        int size = list.size();
        for (int j = 0; j < size; j++) {
            List<String> additional = getAdditional(list.get(j));
            if (additional != null) {
                list.add(additional.get(0));
            }
        }
    }
}
]]></code>
    </test-code>

    <test-code>
        <description>size variable not effectively final</description>
        <expected-problems>0</expected-problems>
        <code><![CDATA[
class Foo {
    void printFirst(int count) {
        String[] array = null;
        int toPrint = array.length;
        if (toPrint > count) {
            toPrint = count;
        }
        for (int i = 0; i < toPrint; i++) {
            System.out.println(array[i]);
        }
    }
}
]]></code>
    </test-code>
  • Try to fix the rule so that these additional test cases pass

@adangel

adangel commented May 14, 2026

Copy link
Copy Markdown
Member

Closing this now because of missing response. Feel free to recreate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:user-input Maintainers are waiting for feedback from author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[java] ForLoopCanBeForeach: False negative when array length is assigned to a pre-declared variable

4 participants