diff --git a/ChangeLog b/ChangeLog index f207eb727..118601d76 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in astroid 4.0.0? ============================ Release date: TBA +* Add support for boolean truthiness constraints (`x`, `not x`) in inference. + + Closes pylint-dev/pylint#9515 + * Fix false positive `invalid-name` on `attrs` classes with `ClassVar` annotated variables. Closes pylint-dev/pylint#10525 diff --git a/astroid/constraint.py b/astroid/constraint.py index 458b55bac..75a5e6aca 100644 --- a/astroid/constraint.py +++ b/astroid/constraint.py @@ -84,6 +84,47 @@ def satisfied_by(self, inferred: InferenceResult) -> bool: return self.negate ^ _matches(inferred, self.CONST_NONE) +class BooleanConstraint(Constraint): + """Represents an "x" or "not x" constraint.""" + + @classmethod + def match( + cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False + ) -> Self | None: + """Return a new constraint for node if expr matches one of these patterns: + + - direct match (expr == node): use given negate value + - negated match (expr == `not node`): flip negate value + + Return None if no pattern matches. + """ + if _matches(expr, node): + return cls(node=node, negate=negate) + + if ( + isinstance(expr, nodes.UnaryOp) + and expr.op == "not" + and _matches(expr.operand, node) + ): + return cls(node=node, negate=not negate) + + return None + + def satisfied_by(self, inferred: InferenceResult) -> bool: + """Return True for uninferable results, or depending on negate flag: + + - negate=False: satisfied if boolean value is True + - negate=True: satisfied if boolean value is False + """ + inferred_booleaness = inferred.bool_value() + if isinstance(inferred, util.UninferableBase) or isinstance( + inferred_booleaness, util.UninferableBase + ): + return True + + return self.negate ^ inferred_booleaness + + def get_constraints( expr: _NameNodes, frame: nodes.LocalsDictNodeNG ) -> dict[nodes.If, set[Constraint]]: @@ -114,7 +155,12 @@ def get_constraints( return constraints_mapping -ALL_CONSTRAINT_CLASSES = frozenset((NoneConstraint,)) +ALL_CONSTRAINT_CLASSES = frozenset( + ( + NoneConstraint, + BooleanConstraint, + ) +) """All supported constraint types.""" diff --git a/tests/test_constraint.py b/tests/test_constraint.py index 63f62754b..84ef498d0 100644 --- a/tests/test_constraint.py +++ b/tests/test_constraint.py @@ -17,6 +17,8 @@ def common_params(node: str) -> pytest.MarkDecorator: ( (f"{node} is None", None, 3), (f"{node} is not None", 3, None), + (f"{node}", 3, None), + (f"not {node}", None, 3), ), ) diff --git a/tests/test_inference.py b/tests/test_inference.py index b8714224e..235563d0a 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -5491,7 +5491,7 @@ def add(x, y): else: kwargs = {} - if nums: + if nums is not None: add(*nums) print(**kwargs) """