diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2021-11-12 16:48:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-12 15:48:58 +0100 |
commit | 1f7d81c7d69e01d6e1d95c967d544f1dcf058213 (patch) | |
tree | ad5218355e6892cbd8aaeee6b21e220d05d939d3 | |
parent | dbfc931234b59a1c3413efa227793d2243878759 (diff) | |
download | astroid-1f7d81c7d69e01d6e1d95c967d544f1dcf058213.tar.gz |
Improve filtering of ``NamedExpr``, particularly within ``If`` nodes (#1233)
* Improve filtering of ``NamedExpr``, particularly within ``If`` nodes
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r-- | ChangeLog | 6 | ||||
-rw-r--r-- | astroid/nodes/node_classes.py | 44 | ||||
-rw-r--r-- | tests/unittest_regrtest.py | 35 |
3 files changed, 82 insertions, 3 deletions
@@ -18,6 +18,12 @@ Release date: TBA * Added missing ``kind`` (for ``Const``) and ``conversion`` (for ``FormattedValue``) fields to repr. +* Fix crash with assignment expressions, nested if expressions and filtering of statements + + Closes PyCQA/pylint#5178 + +* Fix incorrect filtering of assignment expressions statements + What's New in astroid 2.8.4? ============================ diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index b8b7c575..3e4ccd11 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -486,8 +486,28 @@ class LookupMixIn: continue if isinstance(assign_type, NamedExpr): - _stmts = [node] - continue + # If the NamedExpr is in an if statement we do some basic control flow inference + if_parent = _get_if_statement_ancestor(assign_type) + if if_parent: + # If the if statement is within another if statement we append the node + # to possible statements + if _get_if_statement_ancestor(if_parent): + optional_assign = False + _stmts.append(node) + _stmt_parents.append(stmt.parent) + # If the if statement is first-level and not within an orelse block + # we know that it will be evaluated + elif not if_parent.is_orelse: + _stmts = [node] + _stmt_parents = [stmt.parent] + # Else we do not known enough about the control flow to be 100% certain + # and we append to possible statements + else: + _stmts.append(node) + _stmt_parents.append(stmt.parent) + else: + _stmts = [node] + _stmt_parents = [stmt.parent] # XXX comment various branches below!!! try: @@ -534,7 +554,7 @@ class LookupMixIn: # An AssignName node overrides previous assignments if: # 1. node's statement always assigns # 2. node and self are in the same block (i.e., has the same parent as self) - if isinstance(node, AssignName): + if isinstance(node, (NamedExpr, AssignName)): if isinstance(stmt, ExceptHandler): # If node's statement is an ExceptHandler, then it is the variable # bound to the caught exception. If self is not contained within @@ -2798,6 +2818,9 @@ class If(mixins.MultiLineBlockMixin, mixins.BlockRangeMixIn, Statement): self.orelse: typing.List[NodeNG] = [] """The contents of the ``else`` block.""" + self.is_orelse: bool = False + """Whether the if-statement is the orelse-block of another if statement.""" + super().__init__(lineno=lineno, col_offset=col_offset, parent=parent) def postinit( @@ -2819,6 +2842,8 @@ class If(mixins.MultiLineBlockMixin, mixins.BlockRangeMixIn, Statement): self.body = body if orelse is not None: self.orelse = orelse + if isinstance(self.parent, If) and self in self.parent.orelse: + self.is_orelse = True @decorators.cachedproperty def blockstart_tolineno(self): @@ -4197,6 +4222,11 @@ class NamedExpr(mixins.AssignTypeMixin, NodeNG): _astroid_fields = ("target", "value") + optional_assign = True + """Whether this node optionally assigns a variable. + + Since NamedExpr are not always called they do not always assign.""" + def __init__( self, lineno: Optional[int] = None, @@ -4795,3 +4825,11 @@ def is_from_decorator(node): if isinstance(parent, Decorators): return True return False + + +def _get_if_statement_ancestor(node: NodeNG) -> Optional[If]: + """Return the first parent node that is an If node (or None)""" + for parent in node.node_ancestors(): + if isinstance(parent, If): + return parent + return None diff --git a/tests/unittest_regrtest.py b/tests/unittest_regrtest.py index 8cee979c..9938429a 100644 --- a/tests/unittest_regrtest.py +++ b/tests/unittest_regrtest.py @@ -22,8 +22,11 @@ import sys import textwrap import unittest +import pytest + from astroid import MANAGER, Instance, nodes, test_utils from astroid.builder import AstroidBuilder, extract_node +from astroid.const import PY38_PLUS from astroid.exceptions import InferenceError from astroid.raw_building import build_module @@ -156,6 +159,38 @@ def test(): base = next(result._proxied.bases[0].infer()) self.assertEqual(base.name, "int") + @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions") + def test_filter_stmts_nested_if(self) -> None: + builder = AstroidBuilder() + data = """ +def test(val): + variable = None + + if val == 1: + variable = "value" + if variable := "value": + pass + + elif val == 2: + variable = "value_two" + variable = "value_two" + + return variable +""" + module = builder.string_build(data, __name__, __file__) + test_func = module["test"] + result = list(test_func.infer_call_result(module)) + assert len(result) == 3 + assert isinstance(result[0], nodes.Const) + assert result[0].value is None + assert result[0].lineno == 3 + assert isinstance(result[1], nodes.Const) + assert result[1].value == "value" + assert result[1].lineno == 7 + assert isinstance(result[1], nodes.Const) + assert result[2].value == "value_two" + assert result[2].lineno == 12 + def test_ancestors_patching_class_recursion(self) -> None: node = AstroidBuilder().string_build( textwrap.dedent( |