diff --git a/doc/whatsnew/fragments/8167.false_positive b/doc/whatsnew/fragments/8167.false_positive new file mode 100644 index 0000000000..e0c341f659 --- /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 cad4c76ea4..c8cf908a48 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -811,6 +811,9 @@ def _uncertain_nodes_in_false_tests( 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 @@ -819,6 +822,38 @@ def _uncertain_nodes_in_false_tests( 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], diff --git a/tests/functional/u/used/used_before_assignment.py b/tests/functional/u/used/used_before_assignment.py index cb6d9c06ca..91212612d1 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 70153f39ac..0a3c9ff2fa 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