pylint-dev/astroid

Invalid variable lookup when walrus operator is used

ostr00000 opened this issue · 1 comments

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 56a65da with Python 3.10.6

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 - 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 there are always 2 statements - that is good.
  • So the problem is in astroid.are_exclusive. 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.