PY-76870 PyTypeChecker - support compatibility checks for TypeVar defaults for both old and new generics syntax

Co-authored-by: Petr Golubev <petr.golubev@jetbrains.com>

GitOrigin-RevId: f2e299002119fa63c72e38eb898449d59a6c8397
This commit is contained in:
Daniil Kalinin
2025-06-01 18:58:36 +00:00
committed by intellij-monorepo-bot
co-authored by Petr Golubev
parent 71f351ce88
commit 4f61fa9fb0
4 changed files with 218 additions and 16 deletions
@@ -1180,6 +1180,8 @@ INSP.type.hints.invalid.type.argument=Invalid type argument
INSP.type.hints.generic.type.alias.is.not.generic.or.already.parameterized=Type alias is not generic or already specialized
INSP.type.hints.default.type.must.be.type.expression=Default type must be a type expression
INSP.type.hints.invalid.type.expression=Invalid type expression
INSP.type.hints.default.type.do.not.match.constraints=Default type of TypeVar must be one of the constraint types
INSP.type.hints.default.type.do.not.match.bounds=Default type of TypeVar is not a subtype of the bound
INSP.type.hints.illegal.callable.format='Callable' must be used as 'Callable[[arg, ...], result]'
INSP.type.hints.illegal.first.parameter='Callable' first parameter must be a parameter expression
INSP.type.hints.parameters.to.generic.types.must.be.types=Parameters to generic types must be types
@@ -1559,6 +1559,12 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
return null;
}
@ApiStatus.Internal
public static @Nullable PyTypeParameterType getTypeParameterTypeFromTypeParameter(@NotNull PyTypeParameter typeParameter,
@NotNull TypeEvalContext context) {
return staticWithCustomContext(context, c -> getTypeParameterTypeFromTypeParameter(typeParameter, c));
}
private static @Nullable PyTypeParameterType getTypeParameterTypeFromTypeParameter(@NotNull PsiElement element, @NotNull Context context) {
if (element instanceof PyTypeParameter typeParameter) {
String name = typeParameter.getName();
@@ -127,7 +127,11 @@ class PyTypeHintsInspection : PyInspection() {
val defaultExpression = typeParameter.defaultExpression
if (defaultExpression == null) return
when(typeParameter.kind) {
PyAstTypeParameter.Kind.TypeVar -> checkTypeVarDefaultType(defaultExpression)
PyAstTypeParameter.Kind.TypeVar -> {
val typeVarType = PyTypingTypeProvider.getTypeParameterTypeFromTypeParameter(typeParameter, myTypeEvalContext) as? PyTypeVarType
?: return
checkTypeVarDefaultType(defaultExpression, typeVarType)
}
PyAstTypeParameter.Kind.ParamSpec -> checkParamSpecDefaultValue(defaultExpression)
PyAstTypeParameter.Kind.TypeVarTuple -> checkTypeVarTupleDefaultValue(defaultExpression, typeParameter)
}
@@ -470,9 +474,12 @@ class PyTypeHintsInspection : PyInspection() {
ProblemHighlightType.GENERIC_ERROR)
}
default?.let { checkTypeVarDefaultType(it) }
// TODO match bounds and constraints
default?.let {
val type = Ref.deref(PyTypingTypeProvider.getType(call, myTypeEvalContext))
if (type is PyTypeVarType) {
checkTypeVarDefaultType(it, type)
}
}
constraints.asSequence().plus(bound).forEach {
if (it != null) {
@@ -490,19 +497,18 @@ class PyTypeHintsInspection : PyInspection() {
}
}
private fun checkTypeVarDefaultType(defaultExpression: PyExpression) {
val type = Ref.deref(PyTypingTypeProvider.getType(defaultExpression, myTypeEvalContext))
when (type) {
is PyParamSpecType -> registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.cannot.be.used.in.default.type.of.type.var", "ParamSpec"))
is PyTypeVarTupleType -> registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.cannot.be.used.in.default.type.of.type.var", "TypeVarTuple"))
private fun checkTypeVarDefaultType(defaultExpression: PyExpression, typeVarType: PyTypeVarType) {
val typeRef = typeVarType.defaultType
if (typeRef == null) {
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.must.be.type.expression"))
return
}
checkIsCorrectTypeExpression(defaultExpression)
}
private fun checkIsCorrectTypeExpression(expression: PyExpression) {
if (PyTypingTypeProvider.getType(expression, myTypeEvalContext) == null) {
registerProblem(expression, PyPsiBundle.message("INSP.type.hints.default.type.must.be.type.expression"))
val defaultType = typeRef.get()
when (defaultType) {
is PyParamSpecType -> registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.cannot.be.used.in.default.type.of.type.var", "ParamSpec"))
is PyTypeVarTupleType -> registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.cannot.be.used.in.default.type.of.type.var", "TypeVarTuple"))
else -> validateTypeVarDefaultType(typeVarType, defaultType, defaultExpression)
}
}
@@ -520,7 +526,9 @@ class PyTypeHintsInspection : PyInspection() {
if (defaultExpression is PyEllipsisLiteralExpression) return
if (defaultExpression is PyListLiteralExpression) {
defaultExpression.elements.forEach {
checkIsCorrectTypeExpression(it)
if (PyTypingTypeProvider.getType(it, myTypeEvalContext) == null) {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.default.type.must.be.type.expression"))
}
}
return
}
@@ -1368,6 +1376,29 @@ class PyTypeHintsInspection : PyInspection() {
.mapNotNull { it.qualifiedName }
.any { names.contains(it) }
}
private fun validateTypeVarDefaultType(typeVarType: PyTypeVarType, defaultType: PyType?, defaultExpression: PyExpression) {
val defaultTypes = when (defaultType) {
is PyTypeVarType -> defaultType.constraints.ifEmpty {
val objectType = PyBuiltinCache.getInstance(defaultExpression).objectType ?: return
listOf(defaultType.bound ?: objectType)
}
else -> listOf(defaultType)
}
when {
typeVarType.bound != null -> {
if (!defaultTypes.all { PyTypeChecker.match (typeVarType.bound, it, myTypeEvalContext) }) {
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.do.not.match.bounds"))
}
}
typeVarType.constraints.isNotEmpty() -> {
if (!typeVarType.constraints.containsAll(defaultTypes)) {
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.do.not.match.constraints"))
}
}
}
}
}
companion object {
@@ -2648,6 +2648,169 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase {
""");
}
// PY-76870
public void testTypeVarDefaultCanBeSubclassOfBound() {
doTestByText("""
from typing import TypeVar, List
T1 = TypeVar('T1', bound=int, default=bool)
""");
}
// PY-76870
public void testTypeVarDefaultCanNotBeSubclassOfConstraint() {
doTestByText("""
from typing import TypeVar, List
T1 = TypeVar('T1', int, str, default=<warning descr="Default type of TypeVar must be one of the constraint types">bool</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultTypeMatchesConstraints() {
doTestByText("""
from typing import TypeVar, List
# Default type matches one of the constraints
T1 = TypeVar('T1', str, int, default=str)
T2 = TypeVar('T2', str, int, default=int)
# Default type doesn't match any of the constraints
T3 = TypeVar('T3', str, int, default=<warning descr="Default type of TypeVar must be one of the constraint types">bool</warning>)
T4 = TypeVar('T4', str, int, default=<warning descr="Default type of TypeVar must be one of the constraint types">List[int]</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultTypeReferringToTypeVarMatchesBound() {
doTestByText("""
from typing import TypeVar
Y1 = TypeVar("Y1", bound=int)
Invalid = TypeVar("Invalid", float, str, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y1</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultTypeReferringToTypeVarMatchesConstraints() {
doTestByText("""
from typing import TypeVar
Y1 = TypeVar("Y1", int, str)
AlsoOk2 = TypeVar("AlsoOk2", int, str, bool, default=Y1) # OK
AlsoInvalid2 = TypeVar("AlsoInvalid2", bool, complex, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y1</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultTypeReferringToTypeVarWithoutConstraints() {
doTestByText("""
from typing import TypeVar
T = TypeVar("T")
Invalid = TypeVar("Invalid", str, int, default=<warning descr="Default type of TypeVar must be one of the constraint types">T</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultBoundMatchedAgainstBound() {
doTestByText("""
from typing import TypeVar
X1 = TypeVar("X1", bound=int)
Ok1 = TypeVar("Ok1", default=X1, bound=float)
""");
}
// PY-76870
public void testTypeVarDefaultBoundNotMatchedAgainstConstraints() {
doTestByText("""
from typing import TypeVar
Y3 = TypeVar("Y3", bound=int)
Invalid3 = TypeVar("Invalid3", str, complex, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y3</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultBoundNotMatchedAgainstBound() {
doTestByText("""
from typing import TypeVar
X1 = TypeVar("X1", bound=int)
Invalid1 = TypeVar("Invalid1", default=<warning descr="Default type of TypeVar is not a subtype of the bound">X1</warning>, bound=str)
""");
}
// PY-76870
public void testTypeVarDefaultConstraintsNotMatchedAgainstBound() {
doTestByText("""
from typing import TypeVar
Y4 = TypeVar("Y4", int, str)
Invalid4 = TypeVar("Invalid4", bound=str, default=<warning descr="Default type of TypeVar is not a subtype of the bound">Y4</warning>)
""");
}
// PY-76870
public void testTypeVarDefaultChecksNewSyntax() {
doTestByText("""
# NOT OK
def foo1[T1: int = <warning descr="Default type of TypeVar is not a subtype of the bound">str</warning>](): ...
def foo2[T1: (int, bool) = <warning descr="Default type of TypeVar must be one of the constraint types">str</warning>](): ...
def foo3[T1: int, T2: str = <warning descr="Default type of TypeVar is not a subtype of the bound">T1</warning>](): ...
def foo4[T1: (int, bool), T2: str = <warning descr="Default type of TypeVar is not a subtype of the bound">T1</warning>](): ...
def foo5[T1: (int, bool), T2: (int, str) = <warning descr="Default type of TypeVar must be one of the constraint types">T1</warning>](): ...
def foo6[T1: (int, str, float), T2: (int, float) = <warning descr="Default type of TypeVar must be one of the constraint types">T1</warning>](): ...
def foo7[T1: (int, str) = <warning descr="Default type of TypeVar must be one of the constraint types">bool</warning>](): ...
# OK
def bar1[T1: int = bool](): ...
def bar2[T1: (int, str) = str](): ...
def bar3[T1: bool, T2: int = T1](): ...
def bar4[T1: (int, str), T2: (str, int) = T1](): ...
def bar5[T1: (int, str), T2: (str, int, float) = T1](): ...
""");
}
// PY-76870
public void testTypeVarDefaultTypeMatchedWithObject() {
doTestByText("""
from typing import TypeVar, Any
T = TypeVar('T')
T1 = TypeVar('T1', bound=object, default=T)
T2 = TypeVar('T2', int, object, default=T)
""");
}
// PY-76870
public void testTypeVarDefaultAnyInConstraintsAndBound() {
doTestByText("""
from typing import TypeVar, Any
T1 = TypeVar("T1", int, str)
Ok1 = TypeVar("Ok1", int, str, Any, default=T1)
T2 = TypeVar("T2", int, str, Any)
Ok2 = TypeVar("Ok2", int, str, Any, default=T2)
T3 = TypeVar("T3", bound=Any)
Ok3 = TypeVar("Ok3", bound=Any, default=T3)
T4 = TypeVar("T4", bound=str)
Ok4 = TypeVar("Ok4", bound=Any, default=T4)
T5 = TypeVar("T5", bound=Any)
Ok5 = TypeVar("Ok5", bound=Any, default=T5)
Y1 = TypeVar("Y1", int, str, Any)
NotOk1 = TypeVar("NotOk1", int, str, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y1</warning>)
Y2 = TypeVar("Y2", bound=Any)
NotOk2 = TypeVar("NotOk2", int, str, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y2</warning>)
Y3 = TypeVar("Y3", bound=str)
NotOk3 = TypeVar("NotOk3", int, Any, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y3</warning>)
Y4 = TypeVar("Y4", str, Any)
NotOk4 = TypeVar("NotOk4", int, Any, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y4</warning>)
Y5 = TypeVar("Y5", bound=Any)
NotOk5 = TypeVar("NotOk5", int, str, Any, default=<warning descr="Default type of TypeVar must be one of the constraint types">Y5</warning>)
""");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {