From 9323cf20afff416dabe61b7f303791a1ce7f2bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:59:29 +0200 Subject: Add control flow check for ``undefined-variable`` in ``if ... else`` Closes #3688 --- ChangeLog | 6 +++ doc/whatsnew/2.12.rst | 6 +++ pylint/checkers/variables.py | 21 +++++++++ .../u/undefined/undefined_variable_py38.py | 51 +++++++++++++++++++++- .../u/undefined/undefined_variable_py38.txt | 2 + 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 9f60f43d1..4e292305f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -47,6 +47,12 @@ Release date: TBA Fixes part of #3688 +* ``undefined-variable`` now correctly triggers for assignment expressions in if ... else statements + This includes a basic form of control flow inference for if ... else statements using + constant boolean values + + Closes #3688 + * Fix bug with importing namespace packages with relative imports Closes #2967 and #5131 diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst index 52b5ba595..6c71ae159 100644 --- a/doc/whatsnew/2.12.rst +++ b/doc/whatsnew/2.12.rst @@ -96,6 +96,12 @@ Other Changes Fixes part of #3688 +* ``undefined-variable`` now correctly triggers for assignment expressions in if ... else statements + This includes a basic form of control flow inference for if ... else statements using + constant boolean values + + Closes #3688 + * Fix double emitting of ``not-callable`` on inferrable ``properties`` Closes #4426 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 4851421b2..360b0af55 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1211,6 +1211,12 @@ class VariablesChecker(BaseChecker): ) if is_first_level_ref: break + elif isinstance(defnode, nodes.NamedExpr): + if isinstance(defnode.parent, nodes.IfExp): + if self._is_never_evaluated(defnode, defnode.parent): + self.add_message( + "undefined-variable", node=node, args=node.name + ) current_consumer.mark_as_consumed(node.name, found_nodes) # check it's not a loop variable used outside the loop @@ -1635,6 +1641,21 @@ class VariablesChecker(BaseChecker): return 2 return 0 + @staticmethod + def _is_never_evaluated( + defnode: nodes.NamedExpr, defnode_parent: nodes.IfExp + ) -> bool: + """Check if a NamedExpr is inside a side of if ... else that never + gets evaluated + """ + inferred_test = utils.safe_infer(defnode_parent.test) + if isinstance(inferred_test, nodes.Const): + if inferred_test.value is True and defnode == defnode_parent.orelse: + return True + if inferred_test.value is False and defnode == defnode_parent.body: + return True + return False + def _ignore_class_scope(self, node): """ Return True if the node is in a local class scope, as an assignment. diff --git a/tests/functional/u/undefined/undefined_variable_py38.py b/tests/functional/u/undefined/undefined_variable_py38.py index 950d17fb4..3c00bfd85 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.py +++ b/tests/functional/u/undefined/undefined_variable_py38.py @@ -1,5 +1,5 @@ """Tests for undefined variable with assignment expressions""" -# pylint: disable=using-constant-test +# pylint: disable=using-constant-test, expression-not-assigned # Tests for annotation of variables and potentially undefinition @@ -48,3 +48,52 @@ def no_parameters_in_function_default() -> None: print(again_no_default) # [undefined-variable] + +# Tests for assignment expressions in if ... else comprehensions + + +[i for i in range(10) if (if_assign_1 := i)] + +print(if_assign_1) + +IF_TWO = [i for i in range(10) if (if_assign_2 := i)] + +print(if_assign_2) + +IF_THREE = next(i for i in range(10) if (if_assign_3 := i)) + +print(if_assign_3) + +IF_FOUR = {i: i for i in range(10) if (if_assign_4 := i)} + +print(if_assign_4) + +IF_FIVE = {i: i if (if_assign_5 := i) else 0 for i in range(10)} +print(if_assign_5) + +{i: i if True else (else_assign_1 := i) for i in range(10)} + +print(else_assign_1) # [undefined-variable] + + +# Tests for assignment expressions in the assignment of comprehensions + +[(assign_assign_1 := i) for i in range(10)] + +print(assign_assign_1) + +COMPREHENSION_TWO =[(assign_assign_2 := i) for i in range(10)] + +print(assign_assign_2) + +COMPREHENSION_THREE = next((assign_assign_3 := i) for i in range(10)) + +print(assign_assign_3) + +COMPREHENSION_FOUR = {i: (assign_assign_4 := i) for i in range(10)} + +print(assign_assign_4) + +COMPREHENSION_FIVE = {i: (else_assign_2 := i) if False else 0 for i in range(10)} + +print(else_assign_2) # [undefined-variable] diff --git a/tests/functional/u/undefined/undefined_variable_py38.txt b/tests/functional/u/undefined/undefined_variable_py38.txt index 50a4944ac..d33473d98 100644 --- a/tests/functional/u/undefined/undefined_variable_py38.txt +++ b/tests/functional/u/undefined/undefined_variable_py38.txt @@ -1,3 +1,5 @@ undefined-variable:17:15:typing_and_self_referncing_assignment_expression:Undefined variable 'var':HIGH undefined-variable:42:6::Undefined variable 'no_default':HIGH undefined-variable:50:6::Undefined variable 'again_no_default':HIGH +undefined-variable:76:6::Undefined variable 'else_assign_1':HIGH +undefined-variable:99:6::Undefined variable 'else_assign_2':HIGH -- cgit v1.2.1