From 9331cae59fff75e70b4a2ca6208a8e49d3602e82 Mon Sep 17 00:00:00 2001 From: Nikita Paniukhin Date: Sun, 15 Feb 2026 22:44:20 +0000 Subject: [PATCH] [python] PY-76868 Upgrade subscription type form checks Merge-request: IJ-MR-190288 Merged-by: Nikita Paniukhin GitOrigin-RevId: e484c7e6249797b66145149eac58b7c6945758b4 --- .../resources/messages/PyPsiBundle.properties | 4 + .../typing/PyTypingTypeProvider.kt | 45 +++- .../inspections/PyTypeHintsInspection.kt | 210 +++++++++++++---- python/testData/typing/ignored.txt | 1 - .../PyTypeHintsInspectionTest.java | 215 +++++++++++++++++- 5 files changed, 409 insertions(+), 66 deletions(-) diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 6c14bbe76274..298e0a9d4d47 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1263,6 +1263,10 @@ INSP.type.hints.at.most.one.unpacked.tuple=Type argument list can have at most o 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 +INSP.type.hints.ellipsis.allowed.only.as.second.argument='...' is allowed only as the second of two arguments +INSP.type.hints.empty.tuple.only.as.lone.argument=Empty tuple is allowed only as a sole argument +INSP.type.hints.ellipsis.cannot.be.used.with.unpacked.type='...' cannot be used with an unpacked 'TypeVarTuple' or tuple +INSP.type.hints.optional.must.have.exactly.one.argument='Optional' must have exactly one argument 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/codeInsight/typing/PyTypingTypeProvider.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt index d2befa59b12f..a6d0dc252473 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt @@ -1654,9 +1654,14 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { private fun getOptionalType(element: PsiElement, context: Context): Ref? { if (element is PySubscriptionExpression) { if (resolvesToQualifiedNames(element.operand, context.typeContext, OPTIONAL)) { - val indexExpr = element.indexExpression - if (indexExpr != null) { - val typeRef: Ref? = getType(indexExpr, context) + val indexExpr = PyPsiUtils.flattenParens(element.indexExpression) + val argExpr: PyExpression? = when (indexExpr) { + is PyTupleExpression -> indexExpr.elements.singleOrNull() ?: indexExpr + else -> indexExpr + } + + if (argExpr != null) { + val typeRef: Ref? = getType(argExpr, context) if (typeRef != null) { return Ref(PyUnionType.union(typeRef.get(), getInstance(element).noneType)) } @@ -2049,7 +2054,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { private fun getUnionType(element: PsiElement, context: Context): Ref? { if (element is PySubscriptionExpression) { if (resolvesToQualifiedNames(element.operand, context.typeContext, UNION)) { - val union = PyUnionType.union(getIndexTypes(element, context)) + val union = PyUnionType.unionOrNever(getIndexTypes(element, context)) return if (union != null) Ref(union) else null } } @@ -2448,7 +2453,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { private fun getIndexTypes(expression: PySubscriptionExpression, context: Context): MutableList { val types: MutableList = ArrayList() - val indexExpr = expression.indexExpression + val indexExpr = PyPsiUtils.flattenParens(expression.indexExpression) if (indexExpr is PyTupleExpression) { for (expr in indexExpr.elements) { types.add(Ref.deref(getType(expr, context))) @@ -2578,12 +2583,34 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { if (operandType is PyClassType) { if (operandType !is PyTupleType && PyNames.TUPLE == operandType.pyClass.qualifiedName) { if (indexExpr is PyTupleExpression) { - val elements = indexExpr.elements - if (elements.size == 2 && elements[1] is PyEllipsisLiteralExpression) { - return PyTupleType.createHomogeneous(element, indexTypes[0]) + val indexElements = indexExpr.elements.map { PyPsiUtils.flattenParens(it) } + + val lastIsEllipsis = + indexElements.isNotEmpty() && indexElements.last() is PyEllipsisLiteralExpression + + if (lastIsEllipsis) { + if (indexElements.size != 2) return null + if (indexElements.first() is PyEllipsisLiteralExpression) return null + + val indexType = indexTypes.first() + if (indexType is PyPositionalVariadicType) return null + + return PyTupleType.createHomogeneous(element, indexType) + } + else { + for (indexElement in indexElements) { + if (indexElement is PyEllipsisLiteralExpression) return null + if (indexElement is PyTupleExpression && indexElement.elements.isEmpty()) { + if (indexElements.size != 1) return null + } + } + return PyTupleType.create(element, indexTypes) } } - return PyTupleType.create(element, indexTypes) + else { + if (indexExpr is PyEllipsisLiteralExpression) return null + return PyTupleType.create(element, indexTypes) + } } if (isGeneric(operandType, context.typeContext)) { 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 f536bed0c75b..cf10282ef307 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 @@ -101,6 +101,7 @@ import com.jetbrains.python.psi.types.PyTypeChecker.collectGenerics import com.jetbrains.python.psi.types.PyTypeChecker.hasGenerics import com.jetbrains.python.psi.types.PyTypeParameterMapping import com.jetbrains.python.psi.types.PyTypeParameterType +import com.jetbrains.python.psi.types.PyTypeParser import com.jetbrains.python.psi.types.PyTypeVarTupleType import com.jetbrains.python.psi.types.PyTypeVarType import com.jetbrains.python.psi.types.PyTypeVarType.Variance @@ -1087,6 +1088,9 @@ class PyTypeHintsInspection : PyInspection() { val typeAliasExtQName = QualifiedName.fromDottedString(PyTypingTypeProvider.TYPE_ALIAS_EXT) val typingSelf = QualifiedName.fromDottedString(PyTypingTypeProvider.SELF) val typingExtSelf = QualifiedName.fromDottedString(PyTypingTypeProvider.SELF_EXT) + val unionQName = QualifiedName.fromDottedString(PyTypingTypeProvider.UNION) + val optionalQName = QualifiedName.fromDottedString(PyTypingTypeProvider.OPTIONAL) + val qNames = PyResolveUtil.resolveImportedElementQNameLocally(operand) var typingOnly = true @@ -1100,8 +1104,14 @@ class PyTypeHintsInspection : PyInspection() { annotatedQName, annotatedExtQName -> checkAnnotatedParameter(index) typeAliasQName, typeAliasExtQName -> reportParameterizedTypeAlias(index) typingSelf, typingExtSelf -> reportParameterizedSelf(index) + unionQName -> checkGenericTypeArguments(node) + optionalQName -> { + checkGenericTypeArguments(node) + checkOptionalParameter(index) + } callableQName -> { callableExists = true + checkGenericTypeArguments(node, isCallable = true) checkCallableParameters(index) } else -> checkGenericTypeParameterization(node) @@ -1166,7 +1176,19 @@ class PyTypeHintsInspection : PyInspection() { }.firstOrNull() when (declaration) { - is PyTargetExpression -> checkTypeAliasParameterization(node, declaration) + is PyTargetExpression -> { + val builtinName = declaration.qualifiedName?.let { PyTypingTypeProvider.BUILTIN_COLLECTION_CLASSES[it] } + if (builtinName != null) { + val builtinType = PyTypeParser.getTypeByName(node, builtinName, myTypeEvalContext) + val builtinTypeClass = (builtinType as? PyClassType)?.pyClass + if (builtinTypeClass != null) { + checkGenericClassParameterization(node, builtinTypeClass) + return + } + } + + checkTypeAliasParameterization(node, declaration) + } is PyTypeAliasStatement -> checkTypeAliasStatementParameterization(node, declaration) is PyClass -> checkGenericClassParameterization(node, declaration) else -> return @@ -1184,9 +1206,14 @@ class PyTypeHintsInspection : PyInspection() { } return } - val typeArguments = checkGenericTypeArguments(node) - if (typeArguments == null || genericDefinitionType.pyClass.qualifiedName == PyNames.TUPLE) return + val typeArguments = checkGenericTypeArguments(node) ?: return + + if (genericDefinitionType.pyClass.qualifiedName == PyNames.TUPLE) { + checkTupleTypeForm(node) + return + } + val typeParameters = genericDefinitionType.elementTypes val typeParameterListRepresentation = typeParameters.joinToString(prefix = "[", postfix = "]") { it.name!! } @@ -1241,51 +1268,105 @@ class PyTypeHintsInspection : PyInspection() { } } + private fun checkGenericTypeArguments(node: PySubscriptionExpression, isCallable: Boolean = false): List? { + val flatIndexExpr = PyPsiUtils.flattenParens(node.indexExpression) ?: return null + val arguments = (flatIndexExpr as? PyTupleExpression)?.elements ?: arrayOf(flatIndexExpr) + val argumentTypes = mutableListOf() - private fun checkGenericTypeArguments(node: PySubscriptionExpression): List? { - val indexExpression = node.indexExpression ?: return null - val parameters = (indexExpression as? PyTupleExpression)?.elements ?: arrayOf(indexExpression) - val typeArgumentTypes = mutableListOf() - - parameters.forEach { - when (it) { + for ((index, argument) in arguments.withIndex()) { + val argumentType = when (val flatArgument = PyPsiUtils.flattenParens(argument)) { is PyReferenceExpression, is PySubscriptionExpression, is PyBinaryExpression, is PyStarExpression, is PyStringLiteralExpression, is PyListLiteralExpression, + is PyCallExpression, -> { - val typeRef = PyTypingTypeProvider.getType(it, myTypeEvalContext) + val typeRef = PyTypingTypeProvider.getType(argument, myTypeEvalContext) if (typeRef == null) { - val shouldReportError = when { - it is PyReferenceExpression -> { - val isUnresolved = PyResolveUtil.resolveDeclaration(it.reference, resolveContext) == null - val isOpaque = PyTypingTypeProvider.resolveToQualifiedNames(it, myTypeEvalContext) + val message = when { + argument is PyReferenceExpression -> { + val isResolved = PyResolveUtil.resolveDeclaration(argument.reference, resolveContext) != null + val isOpaque = PyTypingTypeProvider.resolveToQualifiedNames(argument, myTypeEvalContext) .any { qName -> PyTypingTypeProvider.OPAQUE_NAMES.contains(qName) } - !isOpaque && !isUnresolved + + if (isResolved && !isOpaque) PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.must.be.types") else null } - else -> true + else -> PyPsiBundle.message("INSP.type.hints.invalid.type.argument") } - if (shouldReportError) { - registerProblem(it, PyPsiBundle.message("INSP.type.hints.invalid.type.argument")) + if (message != null) { + registerProblem(flatArgument, message, ProblemHighlightType.GENERIC_ERROR) } } - typeArgumentTypes.add(Ref.deref(typeRef)) + Ref.deref(typeRef) } is PyNoneLiteralExpression -> { - typeArgumentTypes.add(PyBuiltinCache.getInstance(node).noneType) + PyBuiltinCache.getInstance(node).noneType } - is PyEllipsisLiteralExpression -> { - typeArgumentTypes.add(null) + is PyEllipsisLiteralExpression if ( + node.isBuiltinTupleTypeForm(myTypeEvalContext) || + node.isParamSpecArgument(index, myTypeEvalContext) || + (isCallable && index == 0) + ) -> { + null + } + is PyTupleExpression if ( + (node.isBuiltinTupleTypeForm(myTypeEvalContext) && flatArgument.elements.isEmpty()) || + (isCallable && index == 0) + ) -> { + null } else -> { - registerProblem(it, PyPsiBundle.message("INSP.type.hints.invalid.type.argument")) - typeArgumentTypes.add(null) + registerProblem(argument, PyPsiBundle.message("INSP.type.hints.invalid.type.argument"), ProblemHighlightType.GENERIC_ERROR) + null + } + } + argumentTypes.add(argumentType) + } + return argumentTypes + } + + private fun checkTupleTypeForm(node: PySubscriptionExpression) { + if (!node.isBuiltinTupleTypeForm(myTypeEvalContext)) return + + val flatIndexExpr = PyPsiUtils.flattenParens(node.indexExpression) + val arguments = (flatIndexExpr as? PyTupleExpression)?.elements ?: arrayOf(flatIndexExpr) + + for ((index, argument) in arguments.withIndex()) { + when (val flatArgument = PyPsiUtils.flattenParens(argument)) { + is PyEllipsisLiteralExpression if (index != arguments.lastIndex || arguments.size != 2) -> { + registerProblem(flatArgument, + PyPsiBundle.message("INSP.type.hints.ellipsis.allowed.only.as.second.argument"), + ProblemHighlightType.GENERIC_ERROR) + } + is PyTupleExpression if flatArgument.elements.isEmpty() && arguments.size != 1 -> { + registerProblem(flatArgument, + PyPsiBundle.message("INSP.type.hints.empty.tuple.only.as.lone.argument"), + ProblemHighlightType.GENERIC_ERROR) } } } - return typeArgumentTypes + + val lastArgument = PyPsiUtils.flattenParens(arguments.lastOrNull()) + if (lastArgument is PyEllipsisLiteralExpression) { + val type = Ref.deref(PyTypingTypeProvider.getType(arguments.first(), myTypeEvalContext)) + if (type is PyPositionalVariadicType) { + registerProblem(lastArgument, + PyPsiBundle.message("INSP.type.hints.ellipsis.cannot.be.used.with.unpacked.type"), + ProblemHighlightType.GENERIC_ERROR) + } + } + } + + private fun checkOptionalParameter(index: PyExpression) { + val flatIndexExpr = PyPsiUtils.flattenParens(index) + val elements = (flatIndexExpr as? PyTupleExpression)?.elements ?: arrayOf(index) + if (elements.size != 1) { + registerProblem(flatIndexExpr, + PyPsiBundle.message("INSP.type.hints.optional.must.have.exactly.one.argument"), + ProblemHighlightType.GENERIC_ERROR) + } } private fun checkTypingGenericParameters(node: PySubscriptionExpression, isProtocol: Boolean) { @@ -1441,25 +1522,25 @@ class PyTypeHintsInspection : PyInspection() { parameters .asSequence() .drop(if (isCallable) 1 else 0) - .forEach { - if (it is PyListLiteralExpression) { - registerProblem(it, - PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.must.be.types"), - ProblemHighlightType.GENERIC_ERROR, - null, - RemoveSquareBracketsQuickFix()) - } - else if (it is PyReferenceExpression && multiFollowAssignmentsChain(it).any { resolved -> resolved is PyListLiteralExpression }) { - registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.must.be.types"), - ProblemHighlightType.GENERIC_ERROR) - } - else if (it is PyStarExpression) { - if (alreadyHaveUnpacking) { - registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.cannot.contain.more.than.one.unpacking"), - ProblemHighlightType.GENERIC_ERROR) + .forEach { argument -> + val flatArgument = PyPsiUtils.flattenParens(argument) + when (flatArgument) { + is PyListLiteralExpression -> { + registerProblem(flatArgument, + PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.must.be.types"), + ProblemHighlightType.GENERIC_ERROR, + null, + RemoveSquareBracketsQuickFix()) } - else { - alreadyHaveUnpacking = true + is PyStarExpression -> { + if (alreadyHaveUnpacking) { + registerProblem(flatArgument, + PyPsiBundle.message("INSP.type.hints.parameters.to.generic.types.cannot.contain.more.than.one.unpacking"), + ProblemHighlightType.GENERIC_ERROR) + } + else { + alreadyHaveUnpacking = true + } } } } @@ -1569,11 +1650,12 @@ class PyTypeHintsInspection : PyInspection() { typeArguments: List, @InspectionMessage message: String, ) { + val flatIndexExpr = PyPsiUtils.flattenParens(node.indexExpression) val mapping = PyTypeParameterMapping.mapByShape(typeParameters, typeArguments, PyTypeParameterMapping.Option.USE_DEFAULTS) if (mapping == null) { - registerProblem(node.indexExpression, message, ProblemHighlightType.WARNING) + registerProblem(flatIndexExpr, message, ProblemHighlightType.WARNING) } else { for (pair in mapping.mappedTypes) { @@ -1582,7 +1664,9 @@ class PyTypeHintsInspection : PyInspection() { if (!matched) { val expectedName = PythonDocumentationProvider.getVerboseTypeName(pair.getFirst(), myTypeEvalContext) val actualName = PythonDocumentationProvider.getTypeName(pair.getSecond(), myTypeEvalContext) - registerProblem(node.indexExpression, PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead", expectedName, actualName), ProblemHighlightType.WARNING) + registerProblem(flatIndexExpr, + PyPsiBundle.message("INSP.type.checker.expected.type.got.type.instead", expectedName, actualName), + ProblemHighlightType.WARNING) return } } @@ -1876,3 +1960,37 @@ class PyTypeHintsInspection : PyInspection() { } } } + +private fun PySubscriptionExpression.isBuiltinTupleTypeForm(context: TypeEvalContext): Boolean { + val operandType = context.getType(operand) + return operandType is PyClassType && operandType !is PyTupleType && operandType.classQName == PyNames.TUPLE +} + +private fun PySubscriptionExpression.isParamSpecArgument(argIndex: Int, context: TypeEvalContext): Boolean { + // Generic class parameterization (class Foo[T, **P]: ...) + val operandType = context.getType(this.operand) as? PyClassType + if (operandType != null) { + val genericDefinitionType = PyTypeChecker.findGenericDefinitionType(operandType.pyClass, context) + if (genericDefinitionType != null) { + val typeParameters = genericDefinitionType.elementTypes + if (argIndex in typeParameters.indices && typeParameters[argIndex] is PyParamSpecType) { + return true + } + } + } + + // PEP-695 type alias: `type Alias[S1, **S2] = ...` + val resolveContext = PyResolveContext.defaultContext(context) + val operandRef = this.operand as? PyReferenceExpression + if (operandRef != null) { + val aliasStatement = PyResolveUtil.resolveDeclaration(operandRef.reference, resolveContext) as? PyTypeAliasStatement + if (aliasStatement != null) { + val typeParams = aliasStatement.typeParameterList?.typeParameters ?: emptyList() + if (argIndex in typeParams.indices) { + return typeParams[argIndex].kind == PyAstTypeParameter.Kind.ParamSpec + } + } + } + + return false +} diff --git a/python/testData/typing/ignored.txt b/python/testData/typing/ignored.txt index b89ced0e2aee..322f44d7cee5 100644 --- a/python/testData/typing/ignored.txt +++ b/python/testData/typing/ignored.txt @@ -46,5 +46,4 @@ specialtypes_any.py specialtypes_never.py specialtypes_promotions.py specialtypes_type.py -tuples_type_form.py typeddicts_extra_items.py \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index bf04e6e7ea43..3bd29cbb6703 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -913,7 +913,7 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { def d(x: AnnotatedExt[AnnotatedExt[str, dict(key="value")], ""]): pass - def e(x: Annotated[str, list[dict(key="value")]]): + def e(x: Annotated[str, list[dict(key="value")]]): pass def f(x: Annotated[dict(key="value"), ""]): @@ -2251,22 +2251,22 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { class A:... - c1 = Clazz[print(), int]() - c2 = Clazz[int, print()]() - c3 = Clazz[1] + c1 = Clazz[print(), int]() + c2 = Clazz[int, print()]() + c3 = Clazz[1] c4 = Clazz["int", "str"] c5 = Clazz[dict[int, str]] - c7 = Clazz[True] - c8 = Clazz[list or set] + c7 = Clazz[True] + c8 = Clazz[list or set] c9 = Clazz[Literal[3]] - c10 = Clazz[var] + c10 = Clazz[var] c11 = Clazz[myInt] c12 = Clazz[myIntOrStr] c13 = Clazz[myIntAlias] c14 = Clazz[A] - c15 = Clazz[{"a": "b"}] - c16 = Clazz[(lambda: int)()] - c17 = Clazz[(int, str)] + c15 = Clazz[{"a": "b"}] + c16 = Clazz[(lambda: int)()] + c17 = Clazz[(int, str)] """); } @@ -3200,6 +3200,171 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { """); } + public void testSubscriptionParenthesesFlattening() { + generateVariableTypeAssertions(new Object[][]{ + {"list[((int))]", "list[int]"}, + // TODO: type: list[Any] + {"list[((int, int))]"}, + + {"tuple[((int, int))]", "tuple[int, int]"}, + {"tuple[((int, int)), int]", "tuple[Any, int]"}, + + {"set[((int))]", "set[int]"}, + // TODO: type: set[Any] + {"set[((int, int))]"}, + + {"dict[((int)), (((str)))]", "dict[int, str]"}, + // TODO: type: dict[Any, Any] + {"dict[((int))]"}, + + {"List[((int))]", "list[int]"}, + // TODO: type: list[Any] + {"List[((int, int))]"}, + + {"Tuple[((int)), (((str)))]", "tuple[int, str]"}, + {"Tuple[((int, int)), int]", "tuple[Any, int]"}, + + {"Set[((int))]", "set[int]"}, + // TODO: type: set[Any] + {"Set[((int, int))]"}, + + {"Dict[((int)), (((str)))]", "dict[int, str]"}, + // TODO: type: dict[Any, Any] + {"Dict[((int))]"}, + + {"Union[((int, (((str)))))]", "int | str"}, + {"Union[((int, int)), (int, int)]", + "Any"}, + + {"Optional[((int))]", "int | None"}, + {"Optional[((int, int))]", "Any"}, + + {"tuple[((int)), ((...))]", "tuple[int, ...]"}, + {"tuple[((...)), ((int))]", "Any"}, + {"tuple[(int,), ...]", "tuple[Any, ...]"}, + + {"C[(((int)))]", "C[int]"}, + {"C2[(((int), (str)))]", "C2[int, str]"}, + }); + } + + public void testSubscriptionEmptyParentheses() { + generateVariableTypeAssertions(new Object[][]{ + {"tuple[()]", "tuple[()]"}, + {"tuple[int, ()]", "Any"}, + + {"tuple[(), int]", "Any"}, + {"tuple[(), ...]", "tuple[Any, ...]"}, + {"tuple[(), ()]", + "Any"}, + + {"Tuple[()]", "tuple[()]"}, + {"Tuple[int, ()]", "Any"}, + + // TODO: type: list[Any] + {"list[()]"}, + // TODO: type: set[Any] + {"set[()]"}, + // TODO: type: dict[Any, Any] + {"dict[(), ()]"}, + + // TODO: type: list[Any] + {"List[()]"}, + // TODO: type: set[Any] + {"Set[()]"}, + // TODO: type: dict[Any, Any] + {"Dict[(), ()]"}, + + {"Union[()]", "Never"}, + + {"Optional[()]", "Any"}, + + // TODO: type: C[Any] + {"C[()]"}, + }); + } + + public void testSubscriptionTypeForm() { + generateVariableTypeAssertions(new Object[][]{ + {"list[((int,))]", "list[int]"}, + + {"tuple[((int,))]", "tuple[int]"}, + {"tuple[(int,), int]", "tuple[Any, int]"}, + {"tuple[(int, str), (int, str)]", + "tuple[Any, Any]"}, + + {"set[((int,))]", "set[int]"}, + + {"dict[((int,)), str]", "dict[Any, str]"}, + {"dict[int, (int, str)]", "dict[int, Any]"}, + // TODO: type: dict[Any, Any] + {"dict[(([int]))]"}, + + {"List[((int,))]", "list[int]"}, + + {"Tuple[((int,))]", "tuple[int]"}, + {"Tuple[(int,), int]", "tuple[Any, int]"}, + + {"Set[((int,))]", "set[int]"}, + + {"Dict[((int,)), str]", "dict[Any, str]"}, + {"Dict[int, (int, str)]", "dict[int, Any]"}, + {"Dict[(([int]))]"}, // TODO: type: dict[Any, Any] + + {"tuple[Tuple[int, str]]", "tuple[tuple[int, str]]"}, + {"Tuple[tuple[int], ...]", "tuple[tuple[int], ...]"}, + {"tuple[*Tuple[*tuple[int]]]", "tuple[int]"}, + {"tuple[int, *Tuple[*Tuple[int, str]], str]", "tuple[int, int, str, str]"}, + {"tuple[*tuple[int], *Tuple[int]]", "tuple[int, int]"}, + + {"Union[((int, int,))]", "int"}, + {"Union[(int, int,), int]", "int | Any"}, + + {"Optional[int, int]", "Any"}, + {"Optional[(int,)]", "int | None"}, + + {"Callable[int, int]", "Any"}, + {"Callable[[int], (([int]))]", "Callable[[int], Any]"}, + + // TODO: type: list[Any] + {"list[(([int]))]"}, + {"List[(([int]))]"}, + + {"C[((int,))]", "C[int]"}, + }); + } + + public void testSubscriptionEllipsisTypeForm() { + generateVariableTypeAssertions(new Object[][]{ + {"tuple[int, ...]", "tuple[int, ...]"}, + + {"tuple[..., int]", "Any"}, + {"tuple[int, int, ...]", "Any"}, + {"tuple[int, ..., int]", "Any"}, + {"tuple[...]", "Any"}, + {"tuple[..., ...]", "Any"}, + + {"set[...]", "set[Any]"}, + {"list[...]", "list[Any]"}, + {"dict[...]"}, // TODO: type: "dict[Any, Any]" + + {"Union[int, ...]", "int | Any"}, + {"Optional[...]", "Any"}, + + {"tuple[*tuple[str], ...]", "Any"}, + {"tuple[*tuple[str, ...], ...]", + "Any"}, + + {"Set[...]", "set[Any]"}, + {"List[...]", "list[Any]"}, + {"Dict[...]"}, // TODO: type: "dict[Any, Any]" + + {"Tuple[int, ...]", "tuple[int, ...]"}, + {"Tuple[...]", "Any"}, + + {"C[...]", "C[Any]"}, + }); + } // PY-84289 public void testExponentialAnalysisTimeWhenMapLookupKeyEqualsVariableName() { @@ -3264,6 +3429,36 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { """); } + private void generateVariableTypeAssertions(@NotNull Object @NotNull [][] cases) { + StringBuilder body = new StringBuilder(); + + for (int i = 0; i < cases.length; i++) { + Object[] c = cases[i]; + + String annotationText = (String)c[0]; + String variableName = "variable_" + (i + 1); + + body.append(variableName).append(": ").append(annotationText).append("\n"); + + if (c.length > 1) { + String expectedTypeText = (String)c[1]; + body.append("assert_type(").append(variableName).append(", ").append(expectedTypeText).append(")\n"); + } + } + + myFixture.enableInspections(PyAssertTypeInspection.class); + + doTestByText( + (""" + from typing import assert_type, Any, Never, Generic, List, Set, Dict, Tuple, Union, Optional, Callable + + class C[T]: ... + class C2[T1, T2]: ... + """ + + body).trim() + ); + } + @NotNull @Override protected Class getInspectionClass() {