PY-79129 Report tuple index out of range

GitOrigin-RevId: 42c382dfaa5fc75244151d24324b479a62aee85f
This commit is contained in:
Petr
2025-02-13 20:03:24 +00:00
committed by intellij-monorepo-bot
parent 7d1376160a
commit bb55a1b4f0
5 changed files with 49 additions and 26 deletions
@@ -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
@@ -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;
@@ -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))
}
@@ -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<Integer> 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<String> 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 <T> @Nullable List<T> getIndexExpressionPossibleValues(@Nullable PyExpression indexExpression,
@NotNull TypeEvalContext context,
@NotNull Class<T> indexType) {
public static <T> @NotNull List<T> getIndexExpressionPossibleValues(@Nullable PyExpression indexExpression,
@NotNull TypeEvalContext context,
@NotNull Class<T> 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<T> 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);
}
@@ -2502,4 +2502,21 @@ def foo(param: str | int) -> TypeGuard[str]:
asdict(<warning descr="Expected type 'DataclassInstance', got 'str' instead">"Bob"</warning>)
""");
}
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[<warning descr="Tuple index out of range">j</warning>]
t[<warning descr="Tuple index out of range">2</warning>]
t[<warning descr="Tuple index out of range">k</warning>]
t[<warning descr="Tuple index out of range">-4</warning>]
def bar(t: tuple[int, ...]):
t[10]
""");
}
}