|
20 | 20 | import org.sonar.check.Rule; |
21 | 21 | import org.sonar.plugins.python.api.PythonSubscriptionCheck; |
22 | 22 | import org.sonar.plugins.python.api.SubscriptionContext; |
| 23 | +import org.sonar.plugins.python.api.cfg.ControlFlowGraph; |
| 24 | +import org.sonar.plugins.python.api.symbols.v2.UsageV2; |
23 | 25 | import org.sonar.plugins.python.api.tree.AssignmentStatement; |
24 | 26 | import org.sonar.plugins.python.api.tree.CallExpression; |
25 | 27 | import org.sonar.plugins.python.api.tree.ClassDef; |
26 | 28 | import org.sonar.plugins.python.api.tree.Decorator; |
27 | 29 | import org.sonar.plugins.python.api.tree.Expression; |
28 | 30 | import org.sonar.plugins.python.api.tree.ExpressionList; |
| 31 | +import org.sonar.plugins.python.api.tree.FunctionDef; |
| 32 | +import org.sonar.plugins.python.api.tree.LambdaExpression; |
29 | 33 | import org.sonar.plugins.python.api.tree.Name; |
| 34 | +import org.sonar.plugins.python.api.tree.ReturnStatement; |
30 | 35 | import org.sonar.plugins.python.api.tree.Tree; |
| 36 | +import org.sonar.plugins.python.api.types.v2.FunctionType; |
31 | 37 | import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher; |
32 | 38 | import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers; |
| 39 | +import org.sonar.python.checks.utils.Expressions; |
33 | 40 |
|
34 | 41 | @Rule(key = "S8514") |
35 | 42 | public class DataclassFieldDefinitionCheck extends PythonSubscriptionCheck { |
@@ -66,11 +73,66 @@ private static boolean isDataclassDecorator(Decorator decorator, SubscriptionCon |
66 | 73 | } |
67 | 74 |
|
68 | 75 | private static void checkStatement(SubscriptionContext ctx, Tree statement) { |
69 | | - if (statement instanceof AssignmentStatement assignmentStatement && !isLikelyIntentionalClassVar(assignmentStatement)) { |
| 76 | + if (statement instanceof AssignmentStatement assignmentStatement && !isLikelyIntentionalClassVar(assignmentStatement) |
| 77 | + && !isMethodAssignment(assignmentStatement.assignedValue(), ctx)) { |
70 | 78 | ctx.addIssue(assignmentStatement, MESSAGE_MISSING_ANNOTATION); |
71 | 79 | } |
72 | 80 | } |
73 | 81 |
|
| 82 | + private static boolean isMethodAssignment(Expression expression, SubscriptionContext ctx) { |
| 83 | + Expression value = Expressions.removeParentheses(expression); |
| 84 | + if (isFunction(value)) { |
| 85 | + return true; |
| 86 | + } |
| 87 | + // Source function return types are not always inferred. Recognize local factories |
| 88 | + // returning a function without exempting ordinary data-producing factory calls. |
| 89 | + if (value instanceof CallExpression call && call.callee() instanceof Name name && name.symbolV2() != null) { |
| 90 | + List<UsageV2> bindings = name.symbolV2().usages().stream().filter(UsageV2::isBindingUsage).toList(); |
| 91 | + return bindings.size() == 1 |
| 92 | + && bindings.get(0).kind() == UsageV2.Kind.FUNC_DECLARATION |
| 93 | + && bindings.get(0).tree().parent() instanceof FunctionDef function |
| 94 | + && isMethodFactory(function, ctx); |
| 95 | + } |
| 96 | + return false; |
| 97 | + } |
| 98 | + |
| 99 | + private static boolean isMethodFactory(FunctionDef function, SubscriptionContext ctx) { |
| 100 | + if (function.asyncKeyword() != null || !function.decorators().isEmpty() || hasDataReturnOrYield(function.body())) { |
| 101 | + return false; |
| 102 | + } |
| 103 | + // Checking explicit returns alone would miss paths that implicitly return None. |
| 104 | + ControlFlowGraph cfg = ctx.cfg(function); |
| 105 | + return cfg != null && !cfg.end().predecessors().isEmpty() |
| 106 | + && cfg.end().predecessors().stream().allMatch(block -> { |
| 107 | + List<Tree> elements = block.elements(); |
| 108 | + return !elements.isEmpty() && returnsFunction(elements.get(elements.size() - 1)); |
| 109 | + }); |
| 110 | + } |
| 111 | + |
| 112 | + private static boolean returnsFunction(Tree tree) { |
| 113 | + return tree instanceof ReturnStatement statement && statement.expressions().size() == 1 |
| 114 | + && isFunction(statement.expressions().get(0)); |
| 115 | + } |
| 116 | + |
| 117 | + private static boolean hasDataReturnOrYield(Tree tree) { |
| 118 | + if (tree.is(Tree.Kind.YIELD_EXPR, Tree.Kind.YIELD_STMT) || (tree instanceof ReturnStatement && !returnsFunction(tree))) { |
| 119 | + return true; |
| 120 | + } |
| 121 | + Tree nestedBody = switch (tree) { |
| 122 | + case FunctionDef nested -> nested.body(); |
| 123 | + case ClassDef nested -> nested.body(); |
| 124 | + case LambdaExpression lambda -> lambda.expression(); |
| 125 | + default -> null; |
| 126 | + }; |
| 127 | + // Traverse arguments, return expressions and defaults, but not nested bodies. |
| 128 | + return tree.children().stream().filter(child -> child != nestedBody).anyMatch(DataclassFieldDefinitionCheck::hasDataReturnOrYield); |
| 129 | + } |
| 130 | + |
| 131 | + private static boolean isFunction(Expression expression) { |
| 132 | + Expression value = Expressions.removeParentheses(expression); |
| 133 | + return value.is(Tree.Kind.LAMBDA) || value.typeV2() instanceof FunctionType; |
| 134 | + } |
| 135 | + |
74 | 136 | private static boolean isLikelyIntentionalClassVar(AssignmentStatement assignment) { |
75 | 137 | List<ExpressionList> lhsList = assignment.lhsExpressions(); |
76 | 138 | if (lhsList.size() != 1) { |
|
0 commit comments