Skip to content

Commit 08c2eb0

Browse files
sonar-nigel[bot]Sonar Vibe Botclaude
authored andcommitted
SONARPY-3926 Rule S8510 Loop variables should not be reused in nested loops (#974)
Co-authored-by: Sonar Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> GitOrigin-RevId: 09507f41abf92dc6af97af8697b92b4b9162c6c2
1 parent 98fd5c3 commit 08c2eb0

File tree

6 files changed

+604
-0
lines changed

6 files changed

+604
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.ArrayList;
20+
import java.util.LinkedHashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import org.sonar.check.Rule;
24+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
25+
import org.sonar.plugins.python.api.SubscriptionContext;
26+
import org.sonar.plugins.python.api.tree.Expression;
27+
import org.sonar.plugins.python.api.tree.ForStatement;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.plugins.python.api.tree.Tree;
30+
import org.sonar.plugins.python.api.tree.Tuple;
31+
32+
@Rule(key = "S8510")
33+
public class NestedLoopVariableReuseCheck extends PythonSubscriptionCheck {
34+
35+
private static final String MESSAGE = "Rename this loop variable; it shadows the outer loop variable \"%s\".";
36+
private static final String SECONDARY_MESSAGE = "Outer loop variable.";
37+
38+
@Override
39+
public void initialize(Context context) {
40+
context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, NestedLoopVariableReuseCheck::checkForStatement);
41+
}
42+
43+
private static void checkForStatement(SubscriptionContext ctx) {
44+
ForStatement forStatement = (ForStatement) ctx.syntaxNode();
45+
List<Name> innerVarNames = extractLoopVarNames(forStatement);
46+
if (innerVarNames.isEmpty()) {
47+
return;
48+
}
49+
Map<Name, List<Name>> shadowedOuterNames = collectShadowedNames(forStatement, innerVarNames);
50+
reportIssues(ctx, shadowedOuterNames);
51+
}
52+
53+
private static Map<Name, List<Name>> collectShadowedNames(ForStatement forStatement, List<Name> innerVarNames) {
54+
Map<Name, List<Name>> shadowedOuterNames = new LinkedHashMap<>();
55+
for (Name innerName : innerVarNames) {
56+
shadowedOuterNames.put(innerName, new ArrayList<>());
57+
}
58+
Tree current = forStatement.parent();
59+
while (current != null) {
60+
if (current.is(Tree.Kind.FUNCDEF, Tree.Kind.CLASSDEF, Tree.Kind.LAMBDA,
61+
Tree.Kind.LIST_COMPREHENSION, Tree.Kind.SET_COMPREHENSION,
62+
Tree.Kind.DICT_COMPREHENSION, Tree.Kind.GENERATOR_EXPR)) {
63+
break;
64+
}
65+
if (current.is(Tree.Kind.FOR_STMT)) {
66+
collectMatchingNames((ForStatement) current, innerVarNames, shadowedOuterNames);
67+
}
68+
current = current.parent();
69+
}
70+
return shadowedOuterNames;
71+
}
72+
73+
private static void collectMatchingNames(ForStatement outerLoop, List<Name> innerVarNames, Map<Name, List<Name>> shadowedOuterNames) {
74+
List<Name> outerVarNames = extractLoopVarNames(outerLoop);
75+
for (Name innerName : innerVarNames) {
76+
for (Name outerName : outerVarNames) {
77+
if (innerName.name().equals(outerName.name())) {
78+
shadowedOuterNames.get(innerName).add(outerName);
79+
}
80+
}
81+
}
82+
}
83+
84+
private static void reportIssues(SubscriptionContext ctx, Map<Name, List<Name>> shadowedOuterNames) {
85+
for (Map.Entry<Name, List<Name>> entry : shadowedOuterNames.entrySet()) {
86+
Name innerName = entry.getKey();
87+
List<Name> outerNames = entry.getValue();
88+
if (!outerNames.isEmpty()) {
89+
var issue = ctx.addIssue(innerName, String.format(MESSAGE, innerName.name()));
90+
for (Name outerName : outerNames) {
91+
issue.secondary(outerName, SECONDARY_MESSAGE);
92+
}
93+
}
94+
}
95+
}
96+
97+
private static List<Name> extractLoopVarNames(ForStatement forStatement) {
98+
List<Name> names = new ArrayList<>();
99+
for (Expression expr : forStatement.expressions()) {
100+
collectNames(expr, names);
101+
}
102+
return names;
103+
}
104+
105+
private static void collectNames(Expression expr, List<Name> names) {
106+
if (expr.is(Tree.Kind.NAME)) {
107+
Name name = (Name) expr;
108+
if (!name.name().startsWith("_")) {
109+
names.add(name);
110+
}
111+
} else if (expr.is(Tree.Kind.TUPLE)) {
112+
for (Expression element : ((Tuple) expr).elements()) {
113+
collectNames(element, names);
114+
}
115+
}
116+
}
117+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ public Stream<Class<?>> getChecks() {
320320
NestedCollectionsCreationCheck.class,
321321
NestedConditionalExpressionCheck.class,
322322
NestedControlFlowDepthCheck.class,
323+
NestedLoopVariableReuseCheck.class,
323324
NewStyleClassCheck.class,
324325
NonCallableCalledCheck.class,
325326
NonStandardCryptographicAlgorithmCheck.class,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<p>This rule raises an issue when a nested loop uses the same variable name as an outer loop, causing the inner loop to shadow the outer loop variable.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>In Python, loop variables persist after the loop completes and can be reassigned by inner loops without any warning. Unlike languages with
4+
block-scoped variables, Python allows inner loops to silently overwrite outer loop variables, making the outer variable inaccessible within the
5+
inner loop scope.</p>
6+
<p>This pattern almost always indicates a programmer error. Common scenarios include copying loop patterns without changing variable names, or
7+
confusion about Python's scoping rules. The shadowing happens silently without any compiler or runtime warning, making these bugs difficult to spot.</p>
8+
<h2>How to fix it</h2>
9+
<p>Use different variable names for each loop. Choose descriptive names that reflect what each variable represents.</p>
10+
<h3>Code examples</h3>
11+
<h4>Noncompliant code example</h4>
12+
<pre data-diff-id="1" data-diff-type="noncompliant">
13+
for i in range(10):
14+
print(f"Outer: {i}")
15+
for i in range(5): # Noncompliant
16+
print(f"Inner: {i}")
17+
# i now contains 4, not the outer loop value
18+
</pre>
19+
<h4>Compliant solution</h4>
20+
<pre data-diff-id="1" data-diff-type="compliant">
21+
for i in range(10):
22+
print(f"Outer: {i}")
23+
for j in range(5):
24+
print(f"Inner: {j}")
25+
# i still contains the outer loop value
26+
</pre>
27+
<h2>Resources</h2>
28+
<h3>Documentation</h3>
29+
<ul>
30+
<li>Python Documentation - <a href="https://docs.python.org/3/reference/executionmodel.html#naming-and-binding">Naming and Binding</a></li>
31+
</ul>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Loop variables should not be reused in nested loops",
3+
"type": "CODE_SMELL",
4+
"code": {
5+
"impacts": {
6+
"MAINTAINABILITY": "MEDIUM"
7+
},
8+
"attribute": "CLEAR"
9+
},
10+
"status": "ready",
11+
"remediation": {
12+
"func": "Constant\/Issue",
13+
"constantCost": "5min"
14+
},
15+
"tags": [
16+
"suspicious"
17+
],
18+
"defaultSeverity": "Major",
19+
"ruleSpecification": "RSPEC-8510",
20+
"sqKey": "S8510",
21+
"scope": "All",
22+
"quickfix": "unknown"
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class NestedLoopVariableReuseCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify(
27+
"src/test/resources/checks/nestedLoopVariableReuse.py",
28+
new NestedLoopVariableReuseCheck());
29+
}
30+
}

0 commit comments

Comments
 (0)