diff --git a/doc/whatsnew/fragments/9815.false_positive b/doc/whatsnew/fragments/9815.false_positive new file mode 100644 index 0000000000..28b4ee483a --- /dev/null +++ b/doc/whatsnew/fragments/9815.false_positive @@ -0,0 +1,3 @@ +Fix used-before-assignment for PEP 695 type aliases and parameters. + +Closes #9815 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 374ae31e80..eba74d4ed5 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1641,6 +1641,13 @@ def is_node_in_type_annotation_context(node: nodes.NodeNG) -> bool: return False +def is_node_in_pep695_type_context(node: nodes.NodeNG) -> nodes.NodeNG | None: + """Check if node is used in a TypeAlias or as part of a type param.""" + return get_node_first_ancestor_of_type( + node, (nodes.TypeAlias, nodes.TypeVar, nodes.ParamSpec, nodes.TypeVarTuple) + ) + + def is_subclass_of(child: nodes.ClassDef, parent: nodes.ClassDef) -> bool: """Check if first node is a subclass of second node. diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index f54153341c..cf3ee61103 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1947,22 +1947,21 @@ def _check_consumer( # Skip postponed evaluation of annotations # and unevaluated annotations inside a function body + # as well as TypeAlias nodes. if not ( self._postponed_evaluation_enabled and ( isinstance(stmt, nodes.AnnAssign) - or ( - isinstance(stmt, nodes.FunctionDef) - and node - not in { - *(stmt.args.defaults or ()), - *(stmt.args.kw_defaults or ()), - } - ) + or isinstance(stmt, nodes.FunctionDef) + and node + not in { + *(stmt.args.defaults or ()), + *(stmt.args.kw_defaults or ()), + } ) - ) and not ( - isinstance(stmt, nodes.AnnAssign) + or isinstance(stmt, nodes.AnnAssign) and utils.get_node_first_ancestor_of_type(stmt, nodes.FunctionDef) + or isinstance(stmt, nodes.TypeAlias) ): self.add_message( "used-before-assignment", @@ -2034,7 +2033,7 @@ def _report_unfound_name_definition( if ( self._postponed_evaluation_enabled and utils.is_node_in_type_annotation_context(node) - ): + ) or utils.is_node_in_pep695_type_context(node): return if self._is_builtin(node.name): return diff --git a/pylint/testutils/functional/find_functional_tests.py b/pylint/testutils/functional/find_functional_tests.py index f2e636687b..392a8f33fb 100644 --- a/pylint/testutils/functional/find_functional_tests.py +++ b/pylint/testutils/functional/find_functional_tests.py @@ -21,6 +21,7 @@ "ext", "regression", "regression_02", + "used_02", } """Direct parent directories that should be ignored.""" diff --git a/tests/functional/u/used/used_before_assignment_py312.py b/tests/functional/u/used/used_before_assignment_py312.py index f47e005b85..d3acb5df45 100644 --- a/tests/functional/u/used/used_before_assignment_py312.py +++ b/tests/functional/u/used/used_before_assignment_py312.py @@ -1,6 +1,23 @@ -"""used-before-assignment re: python 3.12 generic typing syntax (PEP 695)""" +"""Tests for used-before-assignment with Python 3.12 generic typing syntax (PEP 695)""" +# pylint: disable = invalid-name,missing-docstring,too-few-public-methods,unused-argument + +from typing import TYPE_CHECKING, Callable -from typing import Callable type Point[T] = tuple[T, ...] type Alias[*Ts] = tuple[*Ts] -type Alias[**P] = Callable[P] +type Alias2[**P] = Callable[P, None] + +type AliasType = int | X | Y + +class X: + pass + +if TYPE_CHECKING: + class Y: ... + +class Good[T: Y]: ... +type OtherAlias[T: Y] = T | None + +# https://github.com/pylint-dev/pylint/issues/9884 +def func[T: Y](x: T) -> None: # [redefined-outer-name] FALSE POSITIVE + ... diff --git a/tests/functional/u/used/used_before_assignment_py312.txt b/tests/functional/u/used/used_before_assignment_py312.txt new file mode 100644 index 0000000000..701f3ca358 --- /dev/null +++ b/tests/functional/u/used/used_before_assignment_py312.txt @@ -0,0 +1 @@ +redefined-outer-name:22:9:22:13:func:Redefining name 'T' from outer scope (line 6):UNDEFINED diff --git a/tests/functional/u/used_02/used_before_assignment_py313.py b/tests/functional/u/used_02/used_before_assignment_py313.py new file mode 100644 index 0000000000..4e9d524540 --- /dev/null +++ b/tests/functional/u/used_02/used_before_assignment_py313.py @@ -0,0 +1,16 @@ +"""Tests for used-before-assignment with Python 3.13 type var defaults (PEP 696)""" +# pylint: disable=missing-docstring,unused-argument,too-few-public-methods + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + class Y: ... + +class Good1[T = Y]: ... +class Good2[*Ts = tuple[int, Y]]: ... +class Good3[**P = [int, Y]]: ... +type Alias[T = Y] = T | None + +# https://github.com/pylint-dev/pylint/issues/9884 +def func[T = Y](x: T) -> None: # [redefined-outer-name] FALSE POSITIVE + ... diff --git a/tests/functional/u/used_02/used_before_assignment_py313.rc b/tests/functional/u/used_02/used_before_assignment_py313.rc new file mode 100644 index 0000000000..eb40154ee5 --- /dev/null +++ b/tests/functional/u/used_02/used_before_assignment_py313.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.13 diff --git a/tests/functional/u/used_02/used_before_assignment_py313.txt b/tests/functional/u/used_02/used_before_assignment_py313.txt new file mode 100644 index 0000000000..d866056056 --- /dev/null +++ b/tests/functional/u/used_02/used_before_assignment_py313.txt @@ -0,0 +1 @@ +redefined-outer-name:15:9:15:14:func:Redefining name 'T' from outer scope (line 12):UNDEFINED