diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 40b92effd45a..280d8b43e68e 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -1076,6 +1076,7 @@ INSP.type.checker.unexpected.argument.from.paramspec=Unexpected argument (from P INSP.type.checker.unfilled.parameter.for.paramspec=Parameter ''{0}'' unfilled (from ParamSpec ''{1}'') INSP.type.checker.unfilled.vararg=Parameter ''{0}'' unfilled, expected ''{1}'' INSP.type.checker.expected.type.from.dunder.set.got.type.instead=Expected type ''{0}'' (from ''__set__''), got ''{1}'' instead +INSP.type.checker.tuple.index.out.of.range=Tuple index out of range # PyTypedDictInspection INSP.NAME.typed.dict=Invalid TypedDict definition and usages diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 1e085908ea7c..6b7206dcbc62 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -21,6 +21,7 @@ import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.inspections.quickfix.PyMakeFunctionReturnTypeQuickFix; import com.jetbrains.python.inspections.quickfix.PyMakeReturnsExplicitFix; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PySubscriptionExpressionImpl; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.types.*; import one.util.streamex.StreamEx; @@ -71,9 +72,20 @@ public class PyTypeCheckerInspection extends PyInspection { @Override public void visitPySubscriptionExpression(@NotNull PySubscriptionExpression node) { // TODO: Support slice PySliceExpressions + + PyType operandType = myTypeEvalContext.getType(node.getOperand()); + if (operandType instanceof PyTupleType tupleType && !tupleType.isHomogeneous()) { + PyExpression indexExpression = node.getIndexExpression(); + for (int index : PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(indexExpression, myTypeEvalContext, Integer.class)) { + int count = tupleType.getElementCount(); + if (index < -count || index >= count) { + registerProblem(indexExpression, PyPsiBundle.message("INSP.type.checker.tuple.index.out.of.range")); + } + } + } // Type check in TypedDict subscription expressions cannot be properly done because each key should have its own value type, // so this case is covered by PyTypedDictInspection - if (myTypeEvalContext.getType(node.getOperand()) instanceof PyTypedDictType) return; + if (operandType instanceof PyTypedDictType) return; // Don't type check __class_getitem__ calls inside type hints. Normally these are not type hinted as a construct // special-cased by type checkers if (PyTypingTypeProvider.isInsideTypeHint(node, myTypeEvalContext)) return; diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt index ca1fbdb39f16..ac83335a4d53 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypedDictInspection.kt @@ -37,7 +37,7 @@ class PyTypedDictInspection : PyInspection() { val indexExpression = node.indexExpression val indexExpressionValueOptions = PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(indexExpression, myTypeEvalContext, String::class.java) - if (indexExpressionValueOptions.isNullOrEmpty()) { + if (indexExpressionValueOptions.isEmpty()) { if (!operandType.isDefinition) { val keyList = operandType.fields.keys.joinToString(transform = { "'$it'" }) registerProblem(indexExpression, PyPsiBundle.message("INSP.typeddict.typeddict.key.must.be.string.literal.expected.one", keyList)) @@ -187,9 +187,7 @@ class PyTypedDictInspection : PyInspection() { if (expr !is PySubscriptionExpression) continue val type = myTypeEvalContext.getType(expr.operand) if (type is PyTypedDictType) { - val possibleIndexValues = PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(expr.indexExpression, myTypeEvalContext, String::class.java) - ?: emptyList() - for (index in possibleIndexValues) { + for (index in PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(expr.indexExpression, myTypeEvalContext, String::class.java)) { if (type.fields[index]?.qualifiers?.isRequired == true) { registerProblem(expr.indexExpression, PyPsiBundle.message("INSP.typeddict.key.cannot.be.deleted", index, type.name)) } @@ -269,9 +267,7 @@ class PyTypedDictInspection : PyInspection() { if (target !is PySubscriptionExpression) return@forEach val targetType = myTypeEvalContext.getType(target.operand) if (targetType !is PyTypedDictType) return@forEach - val possibleIndexValues = PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(target.indexExpression, myTypeEvalContext, String::class.java) - ?: emptyList() - for (indexString in possibleIndexValues) { + for (indexString in PySubscriptionExpressionImpl.getIndexExpressionPossibleValues(target.indexExpression, myTypeEvalContext, String::class.java)) { if (targetType.fields[indexString]?.qualifiers?.isReadOnly == true) { registerProblem(target, PyPsiBundle.message("INSP.typeddict.typeddict.field.is.readonly", indexString)) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java index 8a307bd168f4..1ce84c51f7ed 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PySubscriptionExpressionImpl.java @@ -51,20 +51,17 @@ public class PySubscriptionExpressionImpl extends PyElementImpl implements PySub final PyType type = indexExpression != null ? context.getType(getOperand()) : null; if (type instanceof PyTupleType tupleType) { List indexPossibleValues = getIndexExpressionPossibleValues(indexExpression, context, Integer.class); - if (indexPossibleValues != null) { - List<@Nullable PyType> possibleTypes = ContainerUtil.map(indexPossibleValues, index -> { - if (!tupleType.isHomogeneous() && index < 0) { - index += tupleType.getElementCount(); - } - return tupleType.getElementType(index); - }); - return PyUnionType.union(possibleTypes); - } - return null; + List<@Nullable PyType> possibleTypes = ContainerUtil.map(indexPossibleValues, index -> { + if (!tupleType.isHomogeneous() && index < 0) { + index += tupleType.getElementCount(); + } + return tupleType.getElementType(index); + }); + return PyUnionType.union(possibleTypes); } if (type instanceof PyTypedDictType typedDictType) { List indexPossibleValues = getIndexExpressionPossibleValues(indexExpression, context, String.class); - return indexPossibleValues != null ? PyUnionType.union(ContainerUtil.map(indexPossibleValues, typedDictType::getElementType)) : null; + return PyUnionType.union(ContainerUtil.map(indexPossibleValues, typedDictType::getElementType)); } if (type instanceof PyClassType) { PyType parameterizedType = Ref.deref(PyTypingTypeProvider.getType(this, context)); @@ -76,11 +73,11 @@ public class PySubscriptionExpressionImpl extends PyElementImpl implements PySub } @ApiStatus.Internal - public static @Nullable List getIndexExpressionPossibleValues(@Nullable PyExpression indexExpression, - @NotNull TypeEvalContext context, - @NotNull Class indexType) { + public static @NotNull List getIndexExpressionPossibleValues(@Nullable PyExpression indexExpression, + @NotNull TypeEvalContext context, + @NotNull Class indexType) { if (indexExpression == null) { - return null; + return List.of(); } T indexExprValue = PyEvaluator.evaluate(indexExpression, indexType); if (indexExprValue != null) { @@ -88,16 +85,16 @@ public class PySubscriptionExpressionImpl extends PyElementImpl implements PySub } PyType type = context.getType(indexExpression); if (type == null) { - return null; + return List.of(); } List result = new ArrayList<>(); for (PyType subType : PyTypeUtil.toStream(type)) { if (!(subType instanceof PyLiteralType literalType)) { - return null; + return List.of(); } T val = PyEvaluator.evaluateNoResolve(literalType.getExpression(), indexType); if (val == null) { - return null; + return List.of(); } result.add(val); } diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 9f717c2b007b..cafe2ae576ca 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -2502,4 +2502,21 @@ def foo(param: str | int) -> TypeGuard[str]: asdict("Bob") """); } + + public void testTupleIndexOutOfRange() { + doTestByText(""" + from typing import Literal + + def foo(t: tuple[int, str], i: Literal[1], j: Literal[3], k: Literal[-3]): + t[i] + t[-1] + t[j] + t[2] + t[k] + t[-4] + + def bar(t: tuple[int, ...]): + t[10] + """); + } }