Skip to content

Commit c354706

Browse files
sonar-nigel[bot]Vibe Bot
authored andcommitted
SONARPY-3847 Fix S8396 FP: do not raise on X | None = Field(...) patterns (#898)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> GitOrigin-RevId: 10a57f35a44eb8dab9eb8106fe1253f064583a64
1 parent 5156192 commit c354706

2 files changed

Lines changed: 16 additions & 60 deletions

File tree

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

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import org.sonar.plugins.python.api.SubscriptionContext;
2323
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
2424
import org.sonar.plugins.python.api.tree.Argument;
25-
import org.sonar.plugins.python.api.tree.BinaryExpression;
2625
import org.sonar.plugins.python.api.tree.CallExpression;
2726
import org.sonar.plugins.python.api.tree.ClassDef;
2827
import org.sonar.plugins.python.api.tree.Expression;
@@ -43,8 +42,6 @@ public class PydanticOptionalFieldDefaultCheck extends PythonSubscriptionCheck {
4342
private static final TypeMatcher IS_PYDANTIC_FIELD = TypeMatchers.isType("pydantic.Field");
4443

4544
private static final TypeMatcher IS_TYPING_OPTIONAL = TypeMatchers.isType("typing.Optional");
46-
private static final TypeMatcher IS_TYPING_UNION = TypeMatchers.isType("typing.Union");
47-
private static final TypeMatcher IS_NONE_TYPE = TypeMatchers.isObjectOfType("NoneType");
4845

4946
@Override
5047
public void initialize(Context context) {
@@ -67,16 +64,12 @@ private static void checkField(SubscriptionContext ctx, AnnotatedAssignment anno
6764
TypeAnnotation annotation = annotatedAssignment.annotation();
6865
Expression annotationExpr = annotation.expression();
6966

67+
if (!isTypingOptional(annotationExpr, ctx)) {
68+
return;
69+
}
70+
7071
Expression assignedValue = annotatedAssignment.assignedValue();
71-
boolean hasFieldWithEllipsis = assignedValue != null && isFieldCallWithEllipsis(ctx, assignedValue);
72-
73-
if (isTypingOptional(annotationExpr, ctx)) {
74-
// Optional[X]: raise when no default, or when Field(...) with ellipsis
75-
if (assignedValue == null || hasFieldWithEllipsis) {
76-
ctx.addIssue(annotationExpr, MESSAGE);
77-
}
78-
} else if (isNullableNonOptional(annotationExpr, ctx) && hasFieldWithEllipsis) {
79-
// X | None or Union[X, None]: only raise when Field(...) with ellipsis
72+
if (assignedValue == null || isFieldCallWithEllipsis(ctx, assignedValue)) {
8073
ctx.addIssue(annotationExpr, MESSAGE);
8174
}
8275
}
@@ -88,36 +81,6 @@ private static boolean isTypingOptional(Expression annotationExpr, SubscriptionC
8881
return false;
8982
}
9083

91-
private static boolean isNullableNonOptional(Expression annotationExpr, SubscriptionContext ctx) {
92-
// Case 1: T | None (BinaryExpression with BITWISE_OR)
93-
if (annotationExpr.is(Tree.Kind.BITWISE_OR)) {
94-
return containsNone((BinaryExpression) annotationExpr, ctx);
95-
}
96-
97-
// Case 2: Union[T, None] (SubscriptionExpression)
98-
if (annotationExpr instanceof SubscriptionExpression subscriptionExpr) {
99-
return IS_TYPING_UNION.isTrueFor(subscriptionExpr.object(), ctx) && subscriptsContainNone(subscriptionExpr, ctx);
100-
}
101-
102-
return false;
103-
}
104-
105-
private static boolean containsNone(BinaryExpression binaryExpr, SubscriptionContext ctx) {
106-
return isNoneExpression(binaryExpr.leftOperand(), ctx) ||
107-
isNoneExpression(binaryExpr.rightOperand(), ctx) ||
108-
(binaryExpr.leftOperand().is(Tree.Kind.BITWISE_OR) && containsNone((BinaryExpression) binaryExpr.leftOperand(), ctx)) ||
109-
(binaryExpr.rightOperand().is(Tree.Kind.BITWISE_OR) && containsNone((BinaryExpression) binaryExpr.rightOperand(), ctx));
110-
}
111-
112-
private static boolean isNoneExpression(Expression expr, SubscriptionContext ctx) {
113-
return IS_NONE_TYPE.isTrueFor(expr, ctx);
114-
}
115-
116-
private static boolean subscriptsContainNone(SubscriptionExpression subscriptionExpr, SubscriptionContext ctx) {
117-
return subscriptionExpr.subscripts().expressions().stream()
118-
.anyMatch(expr -> isNoneExpression(expr, ctx));
119-
}
120-
12184
private static boolean isFieldCallWithEllipsis(SubscriptionContext ctx, Expression assignedValue) {
12285
if (!(assignedValue instanceof CallExpression callExpr)) {
12386
return false;

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

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,6 @@ class ModelWithMethods(BaseModel):
2121
def some_method(self):
2222
pass
2323

24-
# Field(...) with ellipsis always raises, even for X | None and Union[X, None]
25-
26-
class BitwiseOrWithFieldEllipsis(BaseModel):
27-
value: str | None = Field(...) # Noncompliant
28-
# ^^^^^^^^^^
29-
30-
class NoneLeftWithFieldEllipsis(BaseModel):
31-
value: None | str = Field(...) # Noncompliant
32-
# ^^^^^^^^^^
33-
34-
class UnionWithFieldEllipsis(BaseModel):
35-
value: Union[str, None] = Field(...) # Noncompliant
36-
# ^^^^^^^^^^^^^^^^
37-
3824
# =====================
3925
# COMPLIANT CASES
4026
# =====================
@@ -52,7 +38,7 @@ class SettingsModelCompliant(BaseModel):
5238

5339
class RequiredModel(BaseModel):
5440
required_field: str
55-
another_field: int = Field(...) # not Optional, no issue
41+
another_field: int = Field(...)
5642

5743
class ConfigModel(BaseModel):
5844
timeout: Optional[int] = 30
@@ -61,22 +47,29 @@ class FactoryModel(BaseModel):
6147
items: Optional[list] = Field(default_factory=list)
6248

6349
class RegularClass:
64-
value: Optional[str] # not a BaseModel
65-
66-
# X | None and Union[X, None] without default: not Optional[X], so no issue
50+
value: Optional[str]
6751

6852
class BitwiseOrNoneCompliant(BaseModel):
6953
reason: int | None
7054

55+
class BitwiseOrNoneWithFieldEllipsisCompliant(BaseModel):
56+
bio: str | None = Field(...)
57+
7158
class BitwiseOrNoneWithFieldDefaultCompliant(BaseModel):
7259
title: str | None = Field(default=None)
7360

7461
class NoneLeftCompliant(BaseModel):
7562
description: None | str
7663

64+
class NoneLeftWithFieldEllipsisCompliant(BaseModel):
65+
value: None | str = Field(...)
66+
7767
class UnionWithNoneCompliant(BaseModel):
7868
data: Union[str, None]
7969

70+
class UnionWithFieldEllipsisCompliant(BaseModel):
71+
value: Union[str, None] = Field(...)
72+
8073
# =====================
8174
# EDGE CASES
8275
# =====================

0 commit comments

Comments
 (0)