aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2021-11-12 16:48:58 +0200
committerGitHub <noreply@github.com>2021-11-12 15:48:58 +0100
commit1f7d81c7d69e01d6e1d95c967d544f1dcf058213 (patch)
treead5218355e6892cbd8aaeee6b21e220d05d939d3
parentdbfc931234b59a1c3413efa227793d2243878759 (diff)
downloadastroid-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--ChangeLog6
-rw-r--r--astroid/nodes/node_classes.py44
-rw-r--r--tests/unittest_regrtest.py35
3 files changed, 82 insertions, 3 deletions
diff --git a/ChangeLog b/ChangeLog
index 4c098507..5c617318 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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(