From 15356dbd977c31b817081306047a58bb51a9bf46 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 10 Aug 2025 02:18:46 +0200 Subject: [PATCH 1/3] Fix used-before-assignment for PEP 695 type aliases + parameters --- doc/whatsnew/fragments/9815.false_positive | 3 ++ pylint/checkers/utils.py | 7 ++++ pylint/checkers/variables.py | 38 +++++++++++-------- .../functional/find_functional_tests.py | 1 + .../u/used/used_before_assignment_py312.py | 23 +++++++---- .../u/used/used_before_assignment_py312.txt | 2 +- .../u/used_02/used_before_assignment_py313.py | 16 ++++++++ .../u/used_02/used_before_assignment_py313.rc | 2 + .../used_02/used_before_assignment_py313.txt | 1 + 9 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 doc/whatsnew/fragments/9815.false_positive create mode 100644 tests/functional/u/used_02/used_before_assignment_py313.py create mode 100644 tests/functional/u/used_02/used_before_assignment_py313.rc create mode 100644 tests/functional/u/used_02/used_before_assignment_py313.txt 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..b0f5d55940 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1927,22 +1927,28 @@ def _check_consumer( # Skip postponed evaluation of annotations # and unevaluated annotations inside a function body - if not ( - self._postponed_evaluation_enabled - and ( + 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 ()), + } + ) + ) + ) + and not ( isinstance(stmt, nodes.AnnAssign) - or ( - isinstance(stmt, nodes.FunctionDef) - and node - not in { - *(stmt.args.defaults or ()), - *(stmt.args.kw_defaults or ()), - } + and utils.get_node_first_ancestor_of_type( + stmt, nodes.FunctionDef ) ) - ) and not ( - isinstance(stmt, nodes.AnnAssign) - and utils.get_node_first_ancestor_of_type(stmt, nodes.FunctionDef) + and not isinstance(stmt, nodes.TypeAlias) ): self.add_message( "used-before-assignment", @@ -2015,12 +2021,12 @@ def _report_unfound_name_definition( Returns True if an error is reported; otherwise, returns False. """ + if self._is_builtin(node.name): + return False if ( self._postponed_evaluation_enabled and utils.is_node_in_type_annotation_context(node) - ): - return False - if self._is_builtin(node.name): + ) or utils.is_node_in_pep695_type_context(node): return False if self._is_variable_annotation_in_function(node): 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 From cf8c800d891a85c56fba9e56dae2fe05ab32176c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:36:47 +0200 Subject: [PATCH 2/3] Revert small unnecessary change --- pylint/checkers/variables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index b0f5d55940..ffd25e175d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2021,13 +2021,13 @@ def _report_unfound_name_definition( Returns True if an error is reported; otherwise, returns False. """ - if self._is_builtin(node.name): - return False 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 if self._is_variable_annotation_in_function(node): return False if self._has_nonlocal_in_enclosing_frame( From 638a9ef21f748ed3552e243e6a6bf9c30e817663 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:52:51 +0200 Subject: [PATCH 3/3] Improve formatting --- pylint/checkers/variables.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index ffd25e175d..ace862ab22 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1927,28 +1927,21 @@ def _check_consumer( # Skip postponed evaluation of annotations # and unevaluated annotations inside a function body - 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 ()), - } - ) - ) - ) - and not ( + # as well as TypeAlias nodes. + if not ( + self._postponed_evaluation_enabled # noqa: RUF021 + and ( isinstance(stmt, nodes.AnnAssign) - and utils.get_node_first_ancestor_of_type( - stmt, nodes.FunctionDef - ) + 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.TypeAlias) + 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",