Skip to content

Commit 1007e82

Browse files
joke1196sonartech
authored andcommitted
SONARPY-5066 Support sentinel union expressions
GitOrigin-RevId: 39b1eb0d8a83c949ea844d724e6f00f86cddf4e2
1 parent 4220fb4 commit 1007e82

5 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎python-checks/src/test/resources/checks/identityComparisonWithCachedTypes.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,13 @@ def ordinary_constants(value):
227227
value is subprocess.STDOUT # Noncompliant
228228
member = subprocess.STDOUT
229229
value is member # Noncompliant
230+
231+
232+
MISSING = sentinel("MISSING")
233+
234+
235+
def sentinel_identity_comparisons(value):
236+
value is MISSING
237+
MISSING is value
238+
value is not MISSING
239+
MISSING is not value

‎python-checks/src/test/resources/checks/identityComparisonWithNewObjects.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,13 @@ def locations_and_messages(param):
159159
param is mylist # Noncompliant {{Replace this "is" operator with "==".}}
160160
# ^^
161161

162+
163+
164+
MISSING = sentinel("MISSING")
165+
166+
167+
def sentinel_identity_comparisons(value):
168+
value is MISSING
169+
MISSING is value
170+
value is not MISSING
171+
MISSING is not value

‎python-frontend/src/main/java/org/sonar/python/semantic/v2/types/TrivialTypeInferenceVisitor.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public class TrivialTypeInferenceVisitor extends BaseTreeVisitor {
127127
private static final TypeInferenceMatcher IS_NONE_OBJECT_TYPE = TypeInferenceMatcher.of(
128128
TypeInferenceMatchers.isObjectSatisfying(TypeInferenceMatchers.withFQN(BuiltinTypes.NONE_TYPE)));
129129

130+
private static final TypeInferenceMatcher IS_SENTINEL_OBJECT_TYPE = TypeInferenceMatcher.of(
131+
TypeInferenceMatchers.isObjectOfType("sentinel"));
132+
130133
private static final TypeInferenceMatcher IS_PRIMITIVE_TYPE = TypeInferenceMatcher.of(
131134
TypeInferenceMatchers.any(
132135
TypeInferenceMatchers.isObjectOfType("int"),
@@ -335,9 +338,9 @@ private boolean isTypeExpression(PythonType type) {
335338
if (type instanceof UnionType unionType) {
336339
return unionType.candidates().stream().allMatch(this::isTypeExpression);
337340
}
338-
// None literals have type ObjectType[NoneType], not ClassType, so we need to handle them explicitly
339-
// to support type expressions like `int | None`
340-
return IS_NONE_OBJECT_TYPE.evaluate(type, typePredicateContext).isTrue();
341+
// None literals and sentinels are ObjectTypes which are valid in type expressions.
342+
return IS_NONE_OBJECT_TYPE.evaluate(type, typePredicateContext).isTrue()
343+
|| IS_SENTINEL_OBJECT_TYPE.evaluate(type, typePredicateContext).isTrue();
341344
}
342345

343346
@Override

‎python-frontend/src/test/java/org/sonar/plugins/python/api/types/v2/matchers/TypeMatcherImplTest.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,21 @@ def __init__(self):
162162
assertThat(objectTypeExpression.typeV2()).isNotInstanceOf(UnknownType.class);
163163
}
164164

165+
@Test
166+
void testCheckForBuiltinSentinelObjectType() {
167+
var project = new TestProject();
168+
Expression sentinelExpression = project.lastExpression("""
169+
MISSING = sentinel("MISSING")
170+
MISSING
171+
""");
172+
173+
SubscriptionContext ctx = mock(SubscriptionContext.class);
174+
when(ctx.typeTable()).thenReturn(project.projectLevelTypeTable());
175+
176+
assertThat(TypeMatchers.isObjectOfType("builtins.sentinel").evaluateFor(sentinelExpression, ctx)).isEqualTo(TriBool.TRUE);
177+
assertThat(sentinelExpression.typeV2()).isNotInstanceOf(UnknownType.class);
178+
}
179+
165180
@Test
166181
void testCheckForFunctionType() {
167182
var project = new TestProject();

‎python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5224,6 +5224,46 @@ void nestedUnionTypeAlias() {
52245224
);
52255225
}
52265226

5227+
@Test
5228+
void sentinelTypeExpressions() {
5229+
FileInput root = inferTypes("""
5230+
MISSING = sentinel("MISSING")
5231+
left = MISSING | None
5232+
right = None | MISSING
5233+
nested = int | MISSING | None
5234+
""");
5235+
5236+
var statements = root.statements().statements();
5237+
var sentinelType = ((AssignmentStatement) statements.get(0)).assignedValue().typeV2();
5238+
assertThat(sentinelType).isInstanceOfSatisfying(ObjectType.class,
5239+
objectType -> assertThat(objectType.unwrappedType()).isInstanceOfSatisfying(ClassType.class,
5240+
classType -> assertThat(classType.fullyQualifiedName()).isEqualTo("sentinel")));
5241+
5242+
for (int statementIndex : List.of(1, 2)) {
5243+
var type = ((AssignmentStatement) statements.get(statementIndex)).assignedValue().typeV2();
5244+
assertThat(type).isInstanceOfSatisfying(UnionType.class,
5245+
union -> assertThat(union.candidates().stream().map(PythonType::unwrappedType).map(FullyQualifiedNameHelper::getFullyQualifiedName))
5246+
.containsExactlyInAnyOrder(Optional.of("sentinel"), Optional.of("NoneType")));
5247+
}
5248+
5249+
var nestedType = ((AssignmentStatement) statements.get(3)).assignedValue().typeV2();
5250+
assertThat(nestedType).isInstanceOfSatisfying(UnionType.class,
5251+
union -> assertThat(union.candidates().stream().map(PythonType::unwrappedType).map(FullyQualifiedNameHelper::getFullyQualifiedName))
5252+
.containsExactlyInAnyOrder(Optional.of("int"), Optional.of("sentinel"), Optional.of("NoneType")));
5253+
}
5254+
5255+
@Test
5256+
void userDefinedSentinelIsNotATypeExpression() {
5257+
FileInput root = inferTypes("""
5258+
class sentinel:
5259+
pass
5260+
result = sentinel() | None
5261+
""");
5262+
5263+
var assignment = (AssignmentStatement) root.statements().statements().get(1);
5264+
assertThat(assignment.assignedValue().typeV2()).isEqualTo(PythonType.UNKNOWN);
5265+
}
5266+
52275267
@Test
52285268
void bitwiseOrOnValuesDoesNotCreateUnionType() {
52295269
FileInput root = inferTypes("""

0 commit comments

Comments
 (0)