summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2023-04-16 20:50:34 -0400
committerGitHub <noreply@github.com>2023-04-16 20:50:34 -0400
commitf45bf090a8e20c9fb09d61ed67d7f885ad354f85 (patch)
treedc3b4375c7599d252b2ac7aae7e2d7ef53748da7
parent4a485e28f0a5118b37550123c79f1f6d0dec42a4 (diff)
downloadpylint-git-f45bf090a8e20c9fb09d61ed67d7f885ad354f85.tar.gz
Fix FP `used-before-assignment` for statements guarded under same test (#8581)
-rw-r--r--doc/whatsnew/fragments/8167.false_positive4
-rw-r--r--pylint/checkers/variables.py35
-rw-r--r--tests/functional/u/used/used_before_assignment.py49
-rw-r--r--tests/functional/u/used/used_before_assignment.txt2
4 files changed, 89 insertions, 1 deletions
diff --git a/doc/whatsnew/fragments/8167.false_positive b/doc/whatsnew/fragments/8167.false_positive
new file mode 100644
index 000000000..e0c341f65
--- /dev/null
+++ b/doc/whatsnew/fragments/8167.false_positive
@@ -0,0 +1,4 @@
+Fix false positive for ``used-before-assignment`` when usage and assignment
+are guarded by the same test in different statements.
+
+Closes #8167
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index d82b5a8c8..8c9f6b6ae 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -811,6 +811,9 @@ scope_type : {self._atomic.scope_type}
continue
outer_if = all_if[-1]
+ if NamesConsumer._node_guarded_by_same_test(node, outer_if):
+ continue
+
# Name defined in the if/else control flow
if NamesConsumer._inferred_to_define_name_raise_or_return(name, outer_if):
continue
@@ -820,6 +823,38 @@ scope_type : {self._atomic.scope_type}
return uncertain_nodes
@staticmethod
+ def _node_guarded_by_same_test(node: nodes.NodeNG, other_if: nodes.If) -> bool:
+ """Identify if `node` is guarded by an equivalent test as `other_if`.
+
+ Two tests are equivalent if their string representations are identical
+ or if their inferred values consist only of constants and those constants
+ are identical, and the if test guarding `node` is not a Name.
+ """
+ other_if_test_as_string = other_if.test.as_string()
+ other_if_test_all_inferred = utils.infer_all(other_if.test)
+ for ancestor in node.node_ancestors():
+ if not isinstance(ancestor, nodes.If):
+ continue
+ if ancestor.test.as_string() == other_if_test_as_string:
+ return True
+ if isinstance(ancestor.test, nodes.Name):
+ continue
+ all_inferred = utils.infer_all(ancestor.test)
+ if len(all_inferred) == len(other_if_test_all_inferred):
+ if any(
+ not isinstance(test, nodes.Const)
+ for test in (*all_inferred, *other_if_test_all_inferred)
+ ):
+ continue
+ if {test.value for test in all_inferred} != {
+ test.value for test in other_if_test_all_inferred
+ }:
+ continue
+ return True
+
+ return False
+
+ @staticmethod
def _uncertain_nodes_in_except_blocks(
found_nodes: list[nodes.NodeNG],
node: nodes.NodeNG,
diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py
index cb6d9c06c..91212612d 100644
--- a/tests/functional/u/used/used_before_assignment.py
+++ b/tests/functional/u/used/used_before_assignment.py
@@ -1,6 +1,6 @@
"""Miscellaneous used-before-assignment cases"""
# pylint: disable=consider-using-f-string, missing-function-docstring
-
+import datetime
MSG = "hello %s" % MSG # [used-before-assignment]
@@ -116,3 +116,50 @@ def turn_on2(**kwargs):
var, *args = (1, "restore_dimmer_state")
print(var, *args)
+
+
+# Variables guarded by the same test when used.
+
+# Always false
+if __name__ == "__main__":
+ PERCENT = 20
+ SALE = True
+
+if __name__ == "__main__":
+ print(PERCENT)
+
+# Different test
+if __name__ is None:
+ print(SALE) # [used-before-assignment]
+
+
+# Ambiguous, but same test
+if not datetime.date.today():
+ WAS_TODAY = True
+
+if not datetime.date.today():
+ print(WAS_TODAY)
+
+
+# Different tests but same inferred values
+# Need falsy values here
+def give_me_zero():
+ return 0
+
+def give_me_nothing():
+ return 0
+
+if give_me_zero():
+ WE_HAVE_ZERO = True
+ ALL_DONE = True
+
+if give_me_nothing():
+ print(WE_HAVE_ZERO)
+
+
+# Different tests, different values
+def give_me_none():
+ return None
+
+if give_me_none():
+ print(ALL_DONE) # [used-before-assignment]
diff --git a/tests/functional/u/used/used_before_assignment.txt b/tests/functional/u/used/used_before_assignment.txt
index 70153f39a..0a3c9ff2f 100644
--- a/tests/functional/u/used/used_before_assignment.txt
+++ b/tests/functional/u/used/used_before_assignment.txt
@@ -6,3 +6,5 @@ used-before-assignment:34:3:34:7::Using variable 'VAR2' before assignment:CONTRO
used-before-assignment:52:3:52:7::Using variable 'VAR4' before assignment:CONTROL_FLOW
used-before-assignment:67:3:67:7::Using variable 'VAR6' before assignment:CONTROL_FLOW
used-before-assignment:102:6:102:11::Using variable 'VAR10' before assignment:CONTROL_FLOW
+used-before-assignment:133:10:133:14::Using variable 'SALE' before assignment:CONTROL_FLOW
+used-before-assignment:165:10:165:18::Using variable 'ALL_DONE' before assignment:CONTROL_FLOW