Skip to content

Commit d8070f4

Browse files
sonar-nigel[bot]sonartech
authored andcommitted
SONARPY-3939 Rule S8512 Class fields should not be defined multiple times (#977)
GitOrigin-RevId: 11a73bd19351302bd185b8ec1b9d3589129c9752
1 parent 62e8a63 commit d8070f4

File tree

6 files changed

+412
-0
lines changed

6 files changed

+412
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 java.util.Optional;
24+
import org.sonar.check.Rule;
25+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
26+
import org.sonar.plugins.python.api.SubscriptionContext;
27+
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
28+
import org.sonar.plugins.python.api.tree.AssignmentStatement;
29+
import org.sonar.plugins.python.api.tree.ClassDef;
30+
import org.sonar.plugins.python.api.tree.Expression;
31+
import org.sonar.plugins.python.api.tree.ExpressionList;
32+
import org.sonar.plugins.python.api.tree.Name;
33+
import org.sonar.plugins.python.api.tree.Statement;
34+
import org.sonar.plugins.python.api.tree.Tree;
35+
import org.sonar.plugins.python.api.tree.Tree.Kind;
36+
37+
@Rule(key = "S8512")
38+
public class ClassFieldDefinedMultipleTimesCheck extends PythonSubscriptionCheck {
39+
40+
@Override
41+
public void initialize(Context context) {
42+
context.registerSyntaxNodeConsumer(Kind.CLASSDEF, ctx -> checkClass(ctx, (ClassDef) ctx.syntaxNode()));
43+
}
44+
45+
private static void checkClass(SubscriptionContext ctx, ClassDef classDef) {
46+
Map<String, List<Tree>> fieldDefinitions = new LinkedHashMap<>();
47+
48+
for (Statement stmt : classDef.body().statements()) {
49+
collectFieldDefinition(stmt, fieldDefinitions);
50+
}
51+
52+
reportDuplicateDefinitions(ctx, fieldDefinitions);
53+
}
54+
55+
private static void collectFieldDefinition(Statement stmt, Map<String, List<Tree>> fieldDefinitions) {
56+
if (stmt.is(Kind.ASSIGNMENT_STMT)) {
57+
getAssignmentName((AssignmentStatement) stmt)
58+
.ifPresent(name -> fieldDefinitions.computeIfAbsent(name.name(), k -> new ArrayList<>()).add(name));
59+
} else if (stmt.is(Kind.ANNOTATED_ASSIGNMENT)) {
60+
getAnnotatedAssignmentName((AnnotatedAssignment) stmt)
61+
.ifPresent(name -> fieldDefinitions.computeIfAbsent(name.name(), k -> new ArrayList<>()).add(name));
62+
}
63+
}
64+
65+
private static Optional<Name> getAssignmentName(AssignmentStatement assignment) {
66+
List<ExpressionList> lhsList = assignment.lhsExpressions();
67+
if (lhsList.size() != 1) {
68+
return Optional.empty();
69+
}
70+
List<Expression> expressions = lhsList.get(0).expressions();
71+
if (expressions.size() == 1 && expressions.get(0).is(Kind.NAME)) {
72+
return Optional.of((Name) expressions.get(0));
73+
}
74+
return Optional.empty();
75+
}
76+
77+
private static Optional<Name> getAnnotatedAssignmentName(AnnotatedAssignment annotated) {
78+
if (annotated.variable().is(Kind.NAME) && annotated.assignedValue() != null) {
79+
return Optional.of((Name) annotated.variable());
80+
}
81+
return Optional.empty();
82+
}
83+
84+
private static void reportDuplicateDefinitions(SubscriptionContext ctx, Map<String, List<Tree>> fieldDefinitions) {
85+
fieldDefinitions.forEach((name, definitions) -> {
86+
for (int i = 0; i < definitions.size() - 1; i++) {
87+
Tree current = definitions.get(i);
88+
Tree next = definitions.get(i + 1);
89+
String message = String.format("Remove this assignment; \"%s\" is assigned again on line %d.", name, next.firstToken().line());
90+
ctx.addIssue(current, message).secondary(next, "Reassignment.");
91+
}
92+
});
93+
}
94+
}

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
@@ -159,6 +159,7 @@ public Stream<Class<?>> getChecks() {
159159
ChildRouterBeforeParentRegistrationCheck.class,
160160
CipherBlockChainingCheck.class,
161161
ClassComplexityCheck.class,
162+
ClassFieldDefinedMultipleTimesCheck.class,
162163
ClassMethodFirstArgumentNameCheck.class,
163164
ClassNameCheck.class,
164165
ClearTextProtocolsCheck.class,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>When a class field is defined multiple times in the same class body, only the last assignment takes effect. All previous assignments are dead code
3+
that serve no purpose.</p>
4+
<p>This pattern typically occurs due to copy-paste errors, refactoring mistakes, merge conflicts, or misunderstanding of how class attributes work.</p>
5+
<h3>What is the potential impact?</h3>
6+
<ul>
7+
<li>Developers may not immediately realize which value is actually used.</li>
8+
<li>Dead code must be read, understood, and maintained even though it has no effect.</li>
9+
<li>The duplicate definition might indicate that two different fields were intended, suggesting a naming error.</li>
10+
</ul>
11+
<h2>How to fix it</h2>
12+
<p>Remove all but the last assignment to the class field.</p>
13+
<h3>Code examples</h3>
14+
<h4>Noncompliant code example</h4>
15+
<pre data-diff-id="1" data-diff-type="noncompliant">
16+
class MyClass:
17+
x = 1
18+
y = 2
19+
x = 3 # Noncompliant: "x" is defined twice
20+
</pre>
21+
<h4>Compliant solution</h4>
22+
<pre data-diff-id="1" data-diff-type="compliant">
23+
class MyClass:
24+
x = 3
25+
y = 2
26+
</pre>
27+
<h2>Resources</h2>
28+
<h3>Documentation</h3>
29+
<ul>
30+
<li>Python Documentation - <a href="https://docs.python.org/3/tutorial/classes.html">Classes</a></li>
31+
<li>Python Documentation - <a href="https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy">The standard type hierarchy</a></li>
32+
</ul>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Class fields should not be defined multiple times",
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-8512",
20+
"sqKey": "S8512",
21+
"scope": "All",
22+
"quickfix": "unknown"
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 ClassFieldDefinedMultipleTimesCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/classFieldDefinedMultipleTimes.py", new ClassFieldDefinedMultipleTimesCheck());
27+
}
28+
29+
}

0 commit comments

Comments
 (0)