From eb116ceb7846ba354b6d3d12f201346e81b8bf44 Mon Sep 17 00:00:00 2001 From: Petr Date: Wed, 10 Dec 2025 15:04:37 +0100 Subject: [PATCH] PY-76859 Report a warning if annotation for the `self` parameter of `__init__` method contains class type variables GitOrigin-RevId: 5ad374adfedaf426b32ddbd6facd077a506c9248 --- .../resources/messages/PyPsiBundle.properties | 1 + .../inspections/PyTypeHintsInspection.kt | 25 +++++++++++++++++-- python/testData/typing/ignored.txt | 1 - .../PyTypeHintsInspectionTest.java | 7 ++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index e688adbae1c5..cabc111aa815 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1259,6 +1259,7 @@ INSP.type.hints.type.statement.incorrect.scope=A 'type' statement can be used on INSP.type.hints.at.most.one.unpacked.tuple=Type argument list can have at most one unpacked TypeVarTuple or unbounded tuple INSP.type.hints.cannot.use.covariant.in.function.param=Covariant type variable cannot be used in parameter type INSP.type.hints.cannot.use.contravariant.in.return.type=Contravariant type variable cannot be used in function return type +INSP.type.hints.cannot.use.class.scope.type.variables.in.annotation.for.self.parameter.of__init__=Class-scoped type variables should not be used in the annotation for 'self' parameter of '__init__' method QFIX.remove.function.annotations=Remove function annotations QFIX.replace.with.target.name=Replace with the target name QFIX.remove.generic.parameters=Remove generic parameters diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index b329f0090088..d14e7271f190 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -400,10 +400,10 @@ class PyTypeHintsInspection : PyInspection() { } } - checkTypeCommentAndParameters(node) checkTypeVarsInFunctionAnnotations(node) reportTypeParametersUsedByOuterScope(node) + checkInitSelfParameterAnnotation(node) } override fun visitPyTargetExpression(node: PyTargetExpression) { @@ -1435,7 +1435,7 @@ class PyTypeHintsInspection : PyInspection() { } else if (hasSelf && actualParametersSize == commentParametersSize) { val actualSelfType = - (myTypeEvalContext.getType(cls!!) as? PyInstantiableType<*>) + (myTypeEvalContext.getType(cls) as? PyInstantiableType<*>) ?.let { if (modifier == PyAstFunction.Modifier.CLASSMETHOD) it.toClass() else it.toInstance() } ?: return @@ -1478,6 +1478,27 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkInitSelfParameterAnnotation(function: PyFunction) { + // Report the use of class-scoped type vars in a type hint for `self` parameter of the `__init__` method, + // which is not allowed + if (function.name != PyNames.INIT) return + + val method = function.asMethod() ?: return + val containingClass = method.containingClass ?: return + + val selfParameter = function.parameterList.parameters.firstOrNull() + if (selfParameter !is PyNamedParameter) return + + val selfAnnotationValue = selfParameter.annotation?.value ?: return + val selfAnnotationType = PyTypingTypeProvider.getType(selfAnnotationValue, myTypeEvalContext) ?: return + + val generics = PyTypeChecker.collectGenerics(selfAnnotationType.get(), myTypeEvalContext) + if (generics.typeVars.any { it.scopeOwner === containingClass }) { + registerProblem(selfAnnotationValue, + PyPsiBundle.message("INSP.type.hints.cannot.use.class.scope.type.variables.in.annotation.for.self.parameter.of__init__")) + } + } + private fun checkTypeArgumentsMatchTypeParameters( node: PySubscriptionExpression, typeParameters: List, diff --git a/python/testData/typing/ignored.txt b/python/testData/typing/ignored.txt index adb9ae529a29..c9cc719aea0d 100644 --- a/python/testData/typing/ignored.txt +++ b/python/testData/typing/ignored.txt @@ -8,7 +8,6 @@ callables_annotation.py callables_kwargs.py callables_protocol.py callables_subtyping.py -constructors_call_init.py constructors_call_new.py constructors_call_type.py constructors_callable.py diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index 4e61a12a7ff9..85ae12b85927 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -373,6 +373,13 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { """); } + public void testSelfAnnotationUsesClassScopedTypeParameters() { + doTestByText(""" + class MyClass[T1, T2]: + def __init__(self: MyClass[T2, T1]) -> None: ... + """); + } + // PY-28249 public void testInstanceAndClassChecksOnAny() { doTestByText("""