@@ -84,6 +84,47 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
84
84
return self .negate ^ _matches (inferred , self .CONST_NONE )
85
85
86
86
87
+ class BooleanConstraint (Constraint ):
88
+ """Represents an "x" or "not x" constraint."""
89
+
90
+ @classmethod
91
+ def match (
92
+ cls , node : _NameNodes , expr : nodes .NodeNG , negate : bool = False
93
+ ) -> Self | None :
94
+ """Return a new constraint for node if expr matches one of these patterns:
95
+
96
+ - direct match (expr == node): use given negate value
97
+ - negated match (expr == `not node`): flip negate value
98
+
99
+ Return None if no pattern matches.
100
+ """
101
+ if _matches (expr , node ):
102
+ return cls (node = node , negate = negate )
103
+
104
+ if (
105
+ isinstance (expr , nodes .UnaryOp )
106
+ and expr .op == "not"
107
+ and _matches (expr .operand , node )
108
+ ):
109
+ return cls (node = node , negate = not negate )
110
+
111
+ return None
112
+
113
+ def satisfied_by (self , inferred : InferenceResult ) -> bool :
114
+ """Return True for uninferable results, or depending on negate flag:
115
+
116
+ - negate=False: satisfied if boolean value is True
117
+ - negate=True: satisfied if boolean value is False
118
+ """
119
+ inferred_booleaness = inferred .bool_value ()
120
+ if isinstance (inferred , util .UninferableBase ) or isinstance (
121
+ inferred_booleaness , util .UninferableBase
122
+ ):
123
+ return True
124
+
125
+ return self .negate ^ inferred_booleaness
126
+
127
+
87
128
def get_constraints (
88
129
expr : _NameNodes , frame : nodes .LocalsDictNodeNG
89
130
) -> dict [nodes .If , set [Constraint ]]:
@@ -114,7 +155,12 @@ def get_constraints(
114
155
return constraints_mapping
115
156
116
157
117
- ALL_CONSTRAINT_CLASSES = frozenset ((NoneConstraint ,))
158
+ ALL_CONSTRAINT_CLASSES = frozenset (
159
+ (
160
+ NoneConstraint ,
161
+ BooleanConstraint ,
162
+ )
163
+ )
118
164
"""All supported constraint types."""
119
165
120
166
0 commit comments