Repository: pylint-dev/astroid

Issue:
Invalid variable lookup when walrus operator is used
### Steps to reproduce
1. Consider following code in `loop_error.py`:
	```
    """Test module"""


	def walrus_in_comprehension_test_2(some_path, module_namespace):
	    """Suspected error"""
	    for mod in some_path.iterdir():
	        print(mod)
	
	    for org_mod in some_path.iterdir():
	        if org_mod.is_dir():
	            if mod := module_namespace.get_mod_from_alias(org_mod.name):
	                new_name = mod.name
	            else:
	                new_name = org_mod.name
	
	            print(new_name)
	```
2. Run `pylint ./loop_error.py`

### Current behavior
A warning appears: ```W0631: Using possibly undefined loop variable 'mod' (undefined-loop-variable)```

### Expected behavior
No warning, because the variable `mod` is always defined.

### `python -c "from astroid import __pkginfo__; print(__pkginfo__.version)"` output
- 2.14.1
- 2.15.0-dev0 on 56a65daf1ba391cc85d1a32a8802cfd0c7b7b2ab with Python 3.10.6


Discussion:
## Investigation
I tried to understand the possible reason for this warning. 

### Minimum reproducible code
It seems ultra-rare case, because we cannot:
- remove first loop (`for mod in some_path.iterdir():`),
- remove first condition (`if org_mod.is_dir():`),
- rewrite walrus operator to standard assignment (`mod :=`),
- remove `else` with body,

otherwise, there is no error.

### Tracked calls
- [`pylint/checkers/variables.py:_loopvar_name:419`](https://github.com/PyCQA/pylint/blob/bd22f2822f9344487357c90e18a8505705c60a29/pylint/checkers/variables.py#L2419) - in this function we got only 1 statement in `astmts` - should be 2,
- I looked at `scope_lookup` functions. Until [`astroid/filter_statements.py:_filter_stmts:201`](https://github.com/PyCQA/astroid/blob/56a65daf1ba391cc85d1a32a8802cfd0c7b7b2ab/astroid/filter_statements.py#L201) there are always 2 statements - that is good.
- So the problem is in [`astroid.are_exclusive`](https://github.com/PyCQA/astroid/blob/56a65daf1ba391cc85d1a32a8802cfd0c7b7b2ab/astroid/nodes/node_classes.py#L140). Probably there is an invalid (old, before Python 3.8) assumption that `ast.IfExp` branches are searched only in `IfExp.body` and `IfExp.orelse` and therefore they must be exclusive. But it turns out that if assignment is in `IfExp.test` (i.e. walrus operator), branches are not exclusive - actually there are always inclusive.

### Simpler minimum reproducible code
It seems that this code is a minimum to reproduce error:
```
from astroid import nodes, extract_node

node_if, node_body, node_or_else = extract_node("""
    if val := True:  #@
        print(val)  #@  
    else:
        print(val)  #@
    """)
node_if: nodes.If
node_walrus = next(node_if.nodes_of_class(nodes.NamedExpr))

assert not nodes.are_exclusive(node_walrus, node_body)
```

 I will try to fix it and add test cases.

Patch:
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -137,10 +137,14 @@ def are_exclusive(stmt1, stmt2, exceptions: list[str] | None = None) -> bool:
             # if the common parent is a If or TryExcept statement, look if
             # nodes are in exclusive branches
             if isinstance(node, If) and exceptions is None:
-                if (
-                    node.locate_child(previous)[1]
-                    is not node.locate_child(children[node])[1]
-                ):
+                c2attr, c2node = node.locate_child(previous)
+                c1attr, c1node = node.locate_child(children[node])
+                if "test" in (c1attr, c2attr):
+                    # If any node is `If.test`, then it must be inclusive with
+                    # the other node (`If.body` and `If.orelse`)
+                    return False
+                if c1attr != c2attr:
+                    # different `If` branches (`If.body` and `If.orelse`)
                     return True
             elif isinstance(node, TryExcept):
                 c2attr, c2node = node.locate_child(previous)


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -4,7 +4,10 @@
 
 import unittest
 
-from astroid import Uninferable, builder, nodes
+import pytest
+
+from astroid import Uninferable, builder, extract_node, nodes
+from astroid.const import PY38_PLUS
 from astroid.exceptions import InferenceError
 
 
@@ -30,6 +33,78 @@ def test_not_exclusive(self) -> None:
         self.assertEqual(nodes.are_exclusive(xass1, xnames[1]), False)
         self.assertEqual(nodes.are_exclusive(xass1, xnames[2]), False)
 
+    @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+    def test_not_exclusive_walrus_operator(self) -> None:
+        node_if, node_body, node_or_else = extract_node(
+            """
+        if val := True:  #@
+            print(val)  #@
+        else:
+            print(val)  #@
+        """
+        )
+        node_if: nodes.If
+        node_walrus = next(node_if.nodes_of_class(nodes.NamedExpr))
+
+        assert nodes.are_exclusive(node_walrus, node_if) is False
+        assert nodes.are_exclusive(node_walrus, node_body) is False
+        assert nodes.are_exclusive(node_walrus, node_or_else) is False
+
+        assert nodes.are_exclusive(node_if, node_body) is False
+        assert nodes.are_exclusive(node_if, node_or_else) is False
+        assert nodes.are_exclusive(node_body, node_or_else) is True
+
+    @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+    def test_not_exclusive_walrus_multiple(self) -> None:
+        node_if, body_1, body_2, or_else_1, or_else_2 = extract_node(
+            """
+        if (val := True) or (val_2 := True):  #@
+            print(val)  #@
+            print(val_2)  #@
+        else:
+            print(val)  #@
+            print(val_2)  #@
+        """
+        )
+        node_if: nodes.If
+        walruses = list(node_if.nodes_of_class(nodes.NamedExpr))
+
+        assert nodes.are_exclusive(node_if, walruses[0]) is False
+        assert nodes.are_exclusive(node_if, walruses[1]) is False
+
+        assert nodes.are_exclusive(walruses[0], walruses[1]) is False
+
+        assert nodes.are_exclusive(walruses[0], body_1) is False
+        assert nodes.are_exclusive(walruses[0], body_2) is False
+        assert nodes.are_exclusive(walruses[1], body_1) is False
+        assert nodes.are_exclusive(walruses[1], body_2) is False
+
+        assert nodes.are_exclusive(walruses[0], or_else_1) is False
+        assert nodes.are_exclusive(walruses[0], or_else_2) is False
+        assert nodes.are_exclusive(walruses[1], or_else_1) is False
+        assert nodes.are_exclusive(walruses[1], or_else_2) is False
+
+    @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+    def test_not_exclusive_walrus_operator_nested(self) -> None:
+        node_if, node_body, node_or_else = extract_node(
+            """
+        if all((last_val := i) % 2 == 0 for i in range(10)): #@
+            print(last_val)  #@
+        else:
+            print(last_val)  #@
+        """
+        )
+        node_if: nodes.If
+        node_walrus = next(node_if.nodes_of_class(nodes.NamedExpr))
+
+        assert nodes.are_exclusive(node_walrus, node_if) is False
+        assert nodes.are_exclusive(node_walrus, node_body) is False
+        assert nodes.are_exclusive(node_walrus, node_or_else) is False
+
+        assert nodes.are_exclusive(node_if, node_body) is False
+        assert nodes.are_exclusive(node_if, node_or_else) is False
+        assert nodes.are_exclusive(node_body, node_or_else) is True
+
     def test_if(self) -> None:
         module = builder.parse(
             """