Skip to content

Commit 3690627

Browse files
joke1196sonartech
authored andcommitted
SONARPY-4017 S8494 Fixing FPs on naming conventions (#1047)
GitOrigin-RevId: 4088afedaa3541b3d30fe804b95102aedd1f3a88
1 parent abc60f7 commit 3690627

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ private static void checkClass(SubscriptionContext ctx) {
110110
}
111111

112112
// Check each instance method
113+
String className = classDef.name().name();
113114
for (FunctionDef functionDef : TreeUtils.topLevelFunctionDefs(classDef)) {
114115
PythonType funcType = functionDef.name().typeV2();
115116
if (!(funcType instanceof FunctionType ft) || !ft.isInstanceMethod()) {
@@ -118,7 +119,7 @@ private static void checkClass(SubscriptionContext ctx) {
118119
List<Parameter> params = TreeUtils.positionalParameters(functionDef);
119120
if (!params.isEmpty()) {
120121
String selfName = params.get(0).name().name();
121-
SelfAttributeAssignmentVisitor visitor = new SelfAttributeAssignmentVisitor(selfName, allowedSlots, ctx);
122+
SelfAttributeAssignmentVisitor visitor = new SelfAttributeAssignmentVisitor(selfName, allowedSlots, className, ctx);
122123
functionDef.body().accept(visitor);
123124
}
124125
}
@@ -298,11 +299,13 @@ private static boolean processParentClass(ClassDef parentClassDef, Set<String> a
298299
private static class SelfAttributeAssignmentVisitor extends BaseTreeVisitor {
299300
private final String selfName;
300301
private final Set<String> allowedSlots;
302+
private final String className;
301303
private final SubscriptionContext ctx;
302304

303-
SelfAttributeAssignmentVisitor(String selfName, Set<String> allowedSlots, SubscriptionContext ctx) {
305+
SelfAttributeAssignmentVisitor(String selfName, Set<String> allowedSlots, String className, SubscriptionContext ctx) {
304306
this.selfName = selfName;
305307
this.allowedSlots = allowedSlots;
308+
this.className = className;
306309
this.ctx = ctx;
307310
}
308311

@@ -346,9 +349,23 @@ private void checkQualifiedExpression(Expression expr) {
346349
return;
347350
}
348351
String attrName = qualifiedExpr.name().name();
349-
if (!allowedSlots.contains(attrName)) {
352+
if (!allowedSlots.contains(attrName) && !allowedSlots.contains(mangledName(attrName))) {
350353
ctx.addIssue(qualifiedExpr.name(), String.format(MESSAGE, attrName));
351354
}
352355
}
356+
357+
/**
358+
* Returns the name-mangled form of a private attribute name within this class.
359+
* Python mangles names starting with two underscores (but not ending with two underscores)
360+
* by stripping leading underscores from the class name and prepending "_ClassName".
361+
* For example, "__den" in class "_Rat" becomes "_Rat__den" (not "__Rat__den").
362+
* See: https://docs.python.org/3/reference/expressions.html#atom-identifiers
363+
*/
364+
private String mangledName(String attrName) {
365+
if (attrName.startsWith("__") && !attrName.endsWith("__")) {
366+
return "_" + className.replaceAll("^_+", "") + attrName;
367+
}
368+
return attrName;
369+
}
353370
}
354371
}

python-checks/src/test/resources/checks/slotsAssignment.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,31 @@ class ConcatenatedSlots:
802802
def __init__(self):
803803
self.x = 1
804804
self.missing = 2 # FN: __slots__ is a binary expression, not a literal
805+
806+
# Compliant: __slots__ uses mangled names and assignments use double-underscore syntax
807+
class Rat(object):
808+
__slots__ = ['_Rat__num', '_Rat__den']
809+
810+
def __init__(self, num=0, den=1):
811+
self.__num = num # Compliant: stored as _Rat__num, which is in __slots__
812+
self.__den = den # Compliant: stored as _Rat__den, which is in __slots__
813+
814+
815+
# Issue raised when attribute is not in slots even with partial mangled entries
816+
class RatMissing(object):
817+
__slots__ = ['_RatMissing__num']
818+
819+
def __init__(self, num=0, den=1):
820+
self.__num = num # Compliant: stored as _RatMissing__num, which is in __slots__
821+
self.__den = den # Noncompliant {{Add "__den" to the class's "__slots__".}}
822+
# ^^^^^
823+
824+
825+
# Compliant: class name starts with underscore — mangling strips leading underscores from class name
826+
# Python mangles _Rat.__den to _Rat__den (not __Rat__den)
827+
class _Rat(object):
828+
__slots__ = ['_Rat__num', '_Rat__den']
829+
830+
def __init__(self, num=0, den=1):
831+
self.__num = num # Compliant: stored as _Rat__num, which is in __slots__
832+
self.__den = den # Compliant: stored as _Rat__den, which is in __slots__

0 commit comments

Comments
 (0)