[java] Fix #6495: Consider length/size in local var for ForLoopCanBeForeach#6521
[java] Fix #6495: Consider length/size in local var for ForLoopCanBeForeach#6521leemeii wants to merge 1 commit into
Conversation
… is stored in local variable
|
Compared to main: (comment created at 2026-03-24 18:34:13+00:00 for 2e69d19) |
adangel
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
|
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 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?
<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>
|
|
Closing this now because of missing response. Feel free to recreate it. |
Describe the PR
This PR fixes a false negative in ForLoopCanBeForeach.
Previously, the rule only recognized loop conditions that directly used
array.lengthorcollection.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.lengthorcollection.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?
./mvnw clean verifypasses (checked automatically by github actions)