diff options
author | Jacob Walls <jacobtylerwalls@gmail.com> | 2023-04-16 20:50:34 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-16 20:50:34 -0400 |
commit | f45bf090a8e20c9fb09d61ed67d7f885ad354f85 (patch) | |
tree | dc3b4375c7599d252b2ac7aae7e2d7ef53748da7 | |
parent | 4a485e28f0a5118b37550123c79f1f6d0dec42a4 (diff) | |
download | pylint-git-f45bf090a8e20c9fb09d61ed67d7f885ad354f85.tar.gz |
Fix FP `used-before-assignment` for statements guarded under same test (#8581)
-rw-r--r-- | doc/whatsnew/fragments/8167.false_positive | 4 | ||||
-rw-r--r-- | pylint/checkers/variables.py | 35 | ||||
-rw-r--r-- | tests/functional/u/used/used_before_assignment.py | 49 | ||||
-rw-r--r-- | tests/functional/u/used/used_before_assignment.txt | 2 |
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 |