@@ -84,6 +84,47 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
8484 return self .negate ^ _matches (inferred , self .CONST_NONE )
8585
8686
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+
87128def get_constraints (
88129 expr : _NameNodes , frame : nodes .LocalsDictNodeNG
89130) -> dict [nodes .If , set [Constraint ]]:
@@ -114,7 +155,12 @@ def get_constraints(
114155 return constraints_mapping
115156
116157
117- ALL_CONSTRAINT_CLASSES = frozenset ((NoneConstraint ,))
158+ ALL_CONSTRAINT_CLASSES = frozenset (
159+ (
160+ NoneConstraint ,
161+ BooleanConstraint ,
162+ )
163+ )
118164"""All supported constraint types."""
119165
120166
0 commit comments