From 564abfaf245fa82facfd2b55120aab1db7aa9ebd Mon Sep 17 00:00:00 2001 From: Erwan Le Forestier Date: Mon, 17 Aug 2026 11:35:59 +0200 Subject: [PATCH 1/2] SONARPY-4540 Handle built-in type assignments --- ...riableAndParameterNameConventionCheck.java | 52 ++++++++++++++++++- ...VariableAndParameterNameIncompatibility.py | 12 +++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java b/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java index ee374a68c..403f11f27 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java +++ b/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java @@ -33,6 +33,8 @@ import org.sonar.plugins.python.api.symbols.v2.SymbolV2; import org.sonar.plugins.python.api.symbols.v2.UsageV2; import org.sonar.plugins.python.api.tree.AssignmentStatement; +import org.sonar.plugins.python.api.tree.CallExpression; +import org.sonar.plugins.python.api.tree.ConditionalExpression; import org.sonar.plugins.python.api.tree.Expression; import org.sonar.plugins.python.api.tree.FunctionDef; import org.sonar.plugins.python.api.tree.Name; @@ -40,6 +42,7 @@ import org.sonar.plugins.python.api.tree.SubscriptionExpression; import org.sonar.plugins.python.api.tree.Tree; import org.sonar.plugins.python.api.types.v2.PythonType; +import org.sonar.python.checks.utils.Expressions; import org.sonar.python.checks.utils.MarimoUtils; import org.sonar.python.semantic.SymbolUtils; import org.sonar.python.tree.TreeUtils; @@ -107,7 +110,8 @@ private void checkName(SymbolV2 symbol, SubscriptionContext ctx) { private boolean isType(SymbolV2 symbolV2) { // TypeV1 and TypeV2 can detect different cases and work complementary to find more issues Symbol symbolV1 = SymbolUtils.symbolV2ToSymbolV1(symbolV2).orElse(null); - return symbolV1 != null && (isExtendingType(symbolV1) || isAssignedFromTyping(symbolV2) || isPythonTypeAClassType(symbolV2)); + return symbolV1 != null && (isExtendingType(symbolV1) || isAssignedFromTyping(symbolV2) || isAssignedFromBuiltinType(symbolV2) + || isPythonTypeAClassType(symbolV2)); } private static boolean isExtendingType(Symbol symbol) { @@ -136,6 +140,52 @@ private static boolean isAssignedFromTyping(SymbolV2 symbol) { return false; } + /** + * Determines whether a local variable is assigned from the built-in {@code type} function. + * + * @param symbol the local variable symbol + * @return {@code true} when an assignment directly produces a class object + */ + private static boolean isAssignedFromBuiltinType(SymbolV2 symbol) { + return symbol.usages().stream() + .filter(u -> u.kind() == UsageV2.Kind.ASSIGNMENT_LHS) + .flatMap(usage -> getAssignedValue(usage.tree())) + .anyMatch(LocalVariableAndParameterNameConventionCheck::isBuiltinTypeAssignment); + } + + /** + * Determines whether an assignment directly produces a class object with {@code type}. + * + * @param assignedValue the expression assigned to the local variable + * @return {@code true} when the expression is a built-in {@code type} call or its {@code None} conditional branch + */ + private static boolean isBuiltinTypeAssignment(Expression assignedValue) { + Expression unwrappedValue = Expressions.removeParentheses(assignedValue); + if (isBuiltinTypeCall(unwrappedValue)) { + return true; + } + if (unwrappedValue instanceof ConditionalExpression conditionalExpression) { + return isBuiltinTypeCall(conditionalExpression.trueExpression()) && conditionalExpression.falseExpression().is(Tree.Kind.NONE) + || isBuiltinTypeCall(conditionalExpression.falseExpression()) && conditionalExpression.trueExpression().is(Tree.Kind.NONE); + } + return false; + } + + /** + * Checks whether an expression is a call to the resolved built-in {@code type} function. + * + * @param expression the expression to inspect + * @return {@code true} when the expression calls the built-in {@code type} function + */ + private static boolean isBuiltinTypeCall(Expression expression) { + Expression unwrappedExpression = Expressions.removeParentheses(expression); + if (unwrappedExpression instanceof CallExpression callExpression) { + Symbol calleeSymbol = callExpression.calleeSymbol(); + return calleeSymbol != null && "type".equals(calleeSymbol.fullyQualifiedName()); + } + return false; + } + private boolean isPythonTypeAClassType(SymbolV2 symbol) { PythonType type = SymbolUtils.getPythonType(symbol); return isDjangoModelTypeCheck.check(type).isTrue(); diff --git a/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py b/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py index 63baf38b4..638be1d07 100644 --- a/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py +++ b/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py @@ -91,6 +91,18 @@ class MyClass: MyClassAlias = MyClass # OK MyTypeVariable: type = unknown_call() # Noncompliant +def builtin_type_assignments(role, roles): + RoleType = type(role) + OptionalRoleType = type(roles[0]) if roles else None + ParenthesizedRoleType = (type(role)) + BadName = wrapper(type(role)) # Noncompliant + +def shadowed_type_assignment(): + def type(value): + return value + + BadName = type("role") # Noncompliant + def ml_names(): X = [1, 2, 3] Y = [0, 1, 0] From 60f932a43bae6e4f3d7fb8fa9a967298c4f0279f Mon Sep 17 00:00:00 2001 From: Erwan Le Forestier Date: Mon, 17 Aug 2026 14:31:30 +0200 Subject: [PATCH 2/2] SONARPY-4540 Detect built-in type in annotated and walrus assignments --- ...VariableAndParameterNameConventionCheck.java | 17 ++++++++++++----- ...alVariableAndParameterNameIncompatibility.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java b/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java index 403f11f27..f6961cda7 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java +++ b/python-checks/src/main/java/org/sonar/python/checks/LocalVariableAndParameterNameConventionCheck.java @@ -32,6 +32,8 @@ import org.sonar.plugins.python.api.symbols.Usage; import org.sonar.plugins.python.api.symbols.v2.SymbolV2; import org.sonar.plugins.python.api.symbols.v2.UsageV2; +import org.sonar.plugins.python.api.tree.AnnotatedAssignment; +import org.sonar.plugins.python.api.tree.AssignmentExpression; import org.sonar.plugins.python.api.tree.AssignmentStatement; import org.sonar.plugins.python.api.tree.CallExpression; import org.sonar.plugins.python.api.tree.ConditionalExpression; @@ -192,12 +194,17 @@ private boolean isPythonTypeAClassType(SymbolV2 symbol) { } private static Stream getAssignedValue(Tree assignmentName) { - var assignmentStmt = TreeUtils.firstAncestorOfClass(assignmentName, AssignmentStatement.class); - if (assignmentStmt != null) { - return Stream.of(assignmentStmt.assignedValue()); - } else { - return Stream.empty(); + Tree assignment = TreeUtils.firstAncestor(assignmentName, + t -> t.is(Tree.Kind.ASSIGNMENT_STMT, Tree.Kind.ANNOTATED_ASSIGNMENT, Tree.Kind.ASSIGNMENT_EXPRESSION)); + Expression assignedValue = null; + if (assignment instanceof AssignmentStatement assignmentStatement) { + assignedValue = assignmentStatement.assignedValue(); + } else if (assignment instanceof AnnotatedAssignment annotatedAssignment) { + assignedValue = annotatedAssignment.assignedValue(); + } else if (assignment instanceof AssignmentExpression assignmentExpression) { + assignedValue = assignmentExpression.expression(); } + return assignedValue != null ? Stream.of(assignedValue) : Stream.empty(); } private static @Nullable Symbol getTypingSymbol(Expression expr) { diff --git a/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py b/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py index 638be1d07..7b637e1e3 100644 --- a/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py +++ b/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py @@ -97,6 +97,17 @@ def builtin_type_assignments(role, roles): ParenthesizedRoleType = (type(role)) BadName = wrapper(type(role)) # Noncompliant +def annotated_builtin_type_assignments(role, roles): + AnnotatedRoleType: type = type(role) + AnnotatedOptionalRoleType: type | None = type(roles[0]) if roles else None + AnnotatedBadName: type = wrapper(type(role)) # Noncompliant + +def walrus_builtin_type_assignments(role): + if (WalrusRoleType := type(role)): + pass + while (WalrusBadName := wrapper(type(role))): # Noncompliant + pass + def shadowed_type_assignment(): def type(value): return value