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 636a139d4e..d87540b59a 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1646,6 +1646,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 52239618a7..ace862ab22 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1927,22 +1927,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 + self._postponed_evaluation_enabled # noqa: RUF021 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) # noqa: RUF021 + and node + not in { + *(stmt.args.defaults or ()), + *(stmt.args.kw_defaults or ()), + } ) - ) and not ( - isinstance(stmt, nodes.AnnAssign) + or isinstance(stmt, nodes.AnnAssign) # noqa: RUF021 and utils.get_node_first_ancestor_of_type(stmt, nodes.FunctionDef) + or isinstance(stmt, nodes.TypeAlias) ): self.add_message( "used-before-assignment", @@ -2018,7 +2017,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 False if self._is_builtin(node.name): return False 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 7da2a8d0b1..d3acb5df45 100644 --- a/tests/functional/u/used/used_before_assignment_py312.py +++ b/tests/functional/u/used/used_before_assignment_py312.py @@ -1,14 +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] - -# pylint: disable = invalid-name, missing-class-docstring, too-few-public-methods +type Alias2[**P] = Callable[P, None] -# https://github.com/pylint-dev/pylint/issues/9815 -type IntOrX = int | X # [used-before-assignment] FALSE POSITIVE +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 index e045f5ae43..701f3ca358 100644 --- a/tests/functional/u/used/used_before_assignment_py312.txt +++ b/tests/functional/u/used/used_before_assignment_py312.txt @@ -1 +1 @@ -used-before-assignment:11:20:11:21::Using variable 'X' before assignment:HIGH +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