diff --git a/doc/whatsnew/fragments/10847.false_negative b/doc/whatsnew/fragments/10847.false_negative new file mode 100644 index 0000000000..7ee0977601 --- /dev/null +++ b/doc/whatsnew/fragments/10847.false_negative @@ -0,0 +1,4 @@ +Fix ``used-before-assignment`` false negative when a variable has a bare type +annotation (without a value) and is only assigned inside ``except`` blocks. + +Closes #10847 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 5add0732c0..f359201465 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -617,40 +617,59 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None: uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # Filter out assignments in an Except clause that the node is not - # contained in, assuming they may fail + # Filter out assignments in except/try blocks, tracking whether any + # uncertainty came from these blocks (as opposed to if/elif tests). + has_except_uncertainty = False if found_nodes: uncertain_nodes = self._uncertain_nodes_in_except_blocks( found_nodes, node, node_statement ) + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # If this node is in a Finally block of a Try/Finally, - # filter out assignments in the try portion, assuming they may fail if found_nodes: uncertain_nodes = ( self._uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks( found_nodes, node_statement, name ) ) + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] - # If this node is in an ExceptHandler, - # filter out assignments in the try portion, assuming they may fail if found_nodes: uncertain_nodes = ( self._uncertain_nodes_in_try_blocks_when_evaluating_except_blocks( found_nodes, node_statement ) ) + has_except_uncertainty = has_except_uncertainty or bool(uncertain_nodes) self.consumed_uncertain[node.name] += uncertain_nodes uncertain_nodes_set = set(uncertain_nodes) found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set] + # Treat bare type annotations (AnnAssign without a value) as uncertain + # when there are uncertain definitions from except/try blocks. + # A bare annotation like `x: int` does not actually assign a value, + # so it should not suppress possibly-used-before-assignment when the + # real assignments are in except blocks that may not execute. + # We only do this for except/try uncertainty, not for if/elif + # uncertainty, because if/elif branches are guaranteed to execute + # one path (the bare annotation masking is correct there). + if found_nodes and has_except_uncertainty: + bare_annotations = [ + n + for n in found_nodes + if isinstance(n.parent, nodes.AnnAssign) and n.parent.value is None + ] + if bare_annotations: + self.consumed_uncertain[name] += bare_annotations + bare_set = set(bare_annotations) + found_nodes = [n for n in found_nodes if n not in bare_set] + return found_nodes def _inferred_to_define_name_raise_or_return( diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.py b/tests/functional/u/used/used_before_assignment_type_annotations.py index f4534fdad3..a33671ee42 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.py +++ b/tests/functional/u/used/used_before_assignment_type_annotations.py @@ -107,3 +107,46 @@ def loop_conditional_annotated_assignment(): data={"cat": "harf"} token: str = data.get("cat") # [possibly-used-before-assignment] print(token) + + +def bare_annotation_with_except_assignment(text): + """A bare type annotation should not suppress used-before-assignment + when the only real assignments are in except blocks. + + https://github.com/pylint-dev/pylint/issues/10847 + """ + err: int + try: + result = int(text) + except ValueError: + err = 1 + result = -1 + if result < 0: + print(err) # [used-before-assignment] + + +def bare_annotation_with_value_and_except(text): + """A type annotation with a value should suppress the warning.""" + err: int = 0 + try: + result = int(text) + except ValueError: + err = 1 + result = -1 + if result < 0: + print(err) + + +def bare_annotation_with_if_elif(axis): + """A bare type annotation with if/elif should not be a false positive. + + Regression test: the pandas pattern where a bare annotation precedes + an if/elif chain that covers all runtime values. + https://github.com/pylint-dev/pylint/pull/10852#pullrequestreview-3872486590 + """ + klass: type + if axis == 0: + klass = int + elif axis == 1: + klass = str + return klass diff --git a/tests/functional/u/used/used_before_assignment_type_annotations.txt b/tests/functional/u/used/used_before_assignment_type_annotations.txt index 9b01f2fa27..c2a4608bd7 100644 --- a/tests/functional/u/used/used_before_assignment_type_annotations.txt +++ b/tests/functional/u/used/used_before_assignment_type_annotations.txt @@ -3,3 +3,4 @@ used-before-assignment:28:10:28:18:value_assignment_after_access:Using variable undefined-variable:62:14:62:17:decorator_returning_incorrect_function.wrapper_with_type_and_no_value:Undefined variable 'var':HIGH possibly-used-before-assignment:97:17:97:21:conditional_annotated_assignment:Possibly using variable 'data' before assignment:CONTROL_FLOW possibly-used-before-assignment:108:17:108:21:loop_conditional_annotated_assignment:Possibly using variable 'data' before assignment:CONTROL_FLOW +used-before-assignment:125:14:125:17:bare_annotation_with_except_assignment:Using variable 'err' before assignment:CONTROL_FLOW