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..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,7 +32,11 @@ 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; 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 +44,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 +112,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,18 +142,69 @@ 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(); } 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 63baf38b4..7b637e1e3 100644 --- a/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py +++ b/python-checks/src/test/resources/checks/localVariableAndParameterNameIncompatibility.py @@ -91,6 +91,29 @@ 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 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 + + BadName = type("role") # Noncompliant + def ml_names(): X = [1, 2, 3] Y = [0, 1, 0]