PY-75759 Implement inspections for allowed type variable default values, scoping and type vars following TypeVarTuple

GitOrigin-RevId: 455cdff457a24523fde47947c1ee5d3bd830d07d
This commit is contained in:
Daniil Kalinin
2024-09-30 13:19:16 +02:00
committed by intellij-monorepo-bot
parent f05119cead
commit a7d5a2deea
6 changed files with 303 additions and 21 deletions

View File

@@ -1136,6 +1136,12 @@ INSP.type.hints.illegal.literal.parameter='Literal' may be parameterized with li
INSP.type.hints.parameters.to.generic.must.all.be.type.variables=Parameters to 'Generic[...]' must all be type variables
INSP.type.hints.parameters.to.generic.must.all.be.unique=Parameters to 'Generic[...]' must all be unique
INSP.type.hints.non.default.type.vars.cannot.follow.defaults=Non-default TypeVars cannot follow ones with defaults
INSP.type.hints.default.type.var.cannot.follow.type.var.tuple=TypeVar with a default value cannot follow TypeVarTuple
INSP.type.hints.default.type.refers.to.type.var.out.of.scope=Type parameter has a default type that refers to one or more type variables that are out of scope
INSP.type.hints.default.type.of.type.var.tuple.must.be.unpacked=Default type of TypeVarTuple must be unpacked
INSP.type.hints.default.type.of.param.spec.must.be.param.spec.or.list.of.types=Default type of ParamSpec must be a ParamSpec type or a list of types
INSP.type.hints.default.type.must.be.type.expression=Default type must be a type expression
INSP.type.hints.default.type.is.out.of.scope=Default type of type parameter ''{0}'' refers to a type variable that is out of scope
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

View File

@@ -121,8 +121,8 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
public static final Set<String> TYPE_DICT_QUALIFIERS = Set.of(REQUIRED, REQUIRED_EXT, NOT_REQUIRED, NOT_REQUIRED_EXT, READONLY, READONLY_EXT);
private static final String UNPACK = "typing.Unpack";
private static final String UNPACK_EXT = "typing_extensions.Unpack";
public static final String UNPACK = "typing.Unpack";
public static final String UNPACK_EXT = "typing_extensions.Unpack";
public static final String SELF = "typing.Self";
public static final String SELF_EXT = "typing_extensions.Self";

View File

@@ -9,10 +9,12 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.ast.PyAstTypeParameter
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.types.PyClassLikeType
import com.jetbrains.python.psi.types.PyTypeParameterType
import com.jetbrains.python.psi.types.PyTypeVarType
import com.jetbrains.python.psi.types.TypeEvalContext
@@ -46,15 +48,22 @@ class PyNewStyleGenericSyntaxInspection : PyInspection() {
override fun visitPyTypeParameterList(node: PyTypeParameterList) {
val typeParameters = node.typeParameters
var lastIsDefault = false
var lastIsTypeVarTuple = false
for (typeParameter in typeParameters) {
if (typeParameter.defaultExpressionText != null) {
lastIsDefault = true
if (lastIsTypeVarTuple && typeParameter.kind == PyAstTypeParameter.Kind.TypeVar) {
registerProblem(typeParameter,
PyPsiBundle.message("INSP.type.hints.default.type.var.cannot.follow.type.var.tuple"),
ProblemHighlightType.GENERIC_ERROR)
}
}
else if (lastIsDefault) {
registerProblem(typeParameter,
PyPsiBundle.message("INSP.type.hints.non.default.type.vars.cannot.follow.defaults"),
ProblemHighlightType.GENERIC_ERROR)
}
lastIsTypeVarTuple = typeParameter.kind == PyAstTypeParameter.Kind.TypeVarTuple
}
}
@@ -127,7 +136,7 @@ class PyNewStyleGenericSyntaxInspection : PyInspection() {
PsiTreeUtil.findChildrenOfAnyType(element, false, PyReferenceExpression::class.java)
.associateWith { Ref.deref(PyTypingTypeProvider.getType(it, myTypeEvalContext)) }
// if declarationElement a.k.a target expression is null then it most likely resolves to type parameter
.filter { (_, v) -> v is PyTypeVarType
.filter { (_, v) -> v is PyTypeParameterType
&& v.declarationElement is PyTargetExpression
&& ScopeUtil.getScopeOwner(v.declarationElement) !is PyTypeAliasStatement }
.forEach { (k, _) ->

View File

@@ -12,6 +12,7 @@ import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.PsiUpdateModCommandQuickFix
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
@@ -22,6 +23,7 @@ import com.jetbrains.python.PyNames
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.ast.PyAstFunction
import com.jetbrains.python.ast.PyAstTypeParameter
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache
import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner
@@ -111,6 +113,16 @@ class PyTypeHintsInspection : PyInspection() {
checkParameterizedBuiltins(node)
}
override fun visitPyTypeParameter(typeParameter: PyTypeParameter) {
val defaultExpression = typeParameter.defaultExpression
if (defaultExpression == null) return
when(typeParameter.kind) {
PyAstTypeParameter.Kind.TypeVar -> checkTypeVarDefaultType(defaultExpression, typeParameter)
PyAstTypeParameter.Kind.ParamSpec -> checkParamSpecDefaultValue(defaultExpression, typeParameter)
PyAstTypeParameter.Kind.TypeVarTuple -> checkTypeVarTupleDefaultValue(defaultExpression, typeParameter)
}
}
private fun checkParameterizedBuiltins(node: PySubscriptionExpression) {
if (LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON39)) return
@@ -327,6 +339,10 @@ class PyTypeHintsInspection : PyInspection() {
PyPsiBundle.message("INSP.type.hints.paramspec.expects.string.literal.as.first.argument"),
PyPsiBundle.message("INSP.type.hints.argument.to.paramspec.must.be.string.equal.to.variable.name"))
}
if (name == "default" && argument != null) {
checkParamSpecDefaultValue(argument, typeParameter = null)
}
}
}
@@ -337,6 +353,9 @@ class PyTypeHintsInspection : PyInspection() {
PyPsiBundle.message("INSP.type.hints.typevar.tuple.expects.string.literal.as.first.argument"),
PyPsiBundle.message("INSP.type.hints.argument.to.typevar.tuple.must.be.string.equal.to.variable.name"))
}
if (name == "default" && argument != null) {
checkTypeVarTupleDefaultValue(argument, typeParameter = null)
}
}
}
@@ -352,6 +371,7 @@ class PyTypeHintsInspection : PyInspection() {
var covariant = false
var contravariant = false
var bound: PyExpression? = null
var default: PyExpression? = null
val constraints = mutableListOf<PyExpression?>()
processMatchedArgument(call) { name, argument ->
@@ -363,6 +383,7 @@ class PyTypeHintsInspection : PyInspection() {
"covariant" -> covariant = PyEvaluator.evaluateAsBoolean(argument, false)
"contravariant" -> contravariant = PyEvaluator.evaluateAsBoolean(argument, false)
"bound" -> bound = argument
"default" -> default = argument
"constraints" -> constraints.add(argument)
}
}
@@ -382,6 +403,10 @@ class PyTypeHintsInspection : PyInspection() {
ProblemHighlightType.GENERIC_ERROR)
}
default?.let { checkIsNotLiteral(it) }
// TODO match bounds and constraints
constraints.asSequence().plus(bound).forEach {
if (it != null) {
val type = PyTypingTypeProvider.getType(it, myTypeEvalContext)?.get()
@@ -398,6 +423,60 @@ class PyTypeHintsInspection : PyInspection() {
}
}
private fun checkTypeVarDefaultType(defaultExpression: PyExpression, typeParameter: PyTypeParameter?) {
checkIsNotLiteral(defaultExpression)
checkDefaultIsInScope(defaultExpression, typeParameter)
}
private fun checkIsNotLiteral(expression: PyExpression) {
if (PyTypingTypeProvider.getType(expression, myTypeEvalContext) == null) {
registerProblem(expression, PyPsiBundle.message("INSP.type.hints.default.type.must.be.type.expression"))
}
}
private fun checkTypeVarTupleDefaultValue(defaultExpression: PyExpression, typeParameter: PyTypeParameter?) {
if ((typeParameter != null && defaultExpression is PyStarExpression) || defaultExpression is PySubscriptionExpression) {
val type = Ref.deref(PyTypingTypeProvider.getType(defaultExpression, myTypeEvalContext))
if (type is PyUnpackedTupleType || type is PyTypeVarTupleType) {
checkDefaultIsInScope(defaultExpression, typeParameter)
return
}
}
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.of.type.var.tuple.must.be.unpacked"))
}
private fun checkParamSpecDefaultValue(defaultExpression: PyExpression, typeParameter: PyTypeParameter?) {
if (defaultExpression is PyNoneLiteralExpression && defaultExpression.isEllipsis) return
if (defaultExpression is PyListLiteralExpression) {
defaultExpression.elements.forEach {
checkIsNotLiteral(it)
checkDefaultIsInScope(it, typeParameter)
}
return
}
if (defaultExpression is PyReferenceExpression) {
val defaultType = Ref.deref(PyTypingTypeProvider.getType(defaultExpression, myTypeEvalContext))
if (defaultType !is PyParamSpecType) {
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.of.param.spec.must.be.param.spec.or.list.of.types"))
}
checkDefaultIsInScope(defaultExpression, typeParameter)
return
}
registerProblem(defaultExpression, PyPsiBundle.message("INSP.type.hints.default.type.of.param.spec.must.be.param.spec.or.list.of.types"))
}
private fun checkDefaultIsInScope(expression: PyExpression?, parent: PsiElement?) {
if (parent is PyTypeParameter && expression != null) {
val defaultOutOfScope =
PyTypeChecker.collectGenerics(Ref.deref(PyTypingTypeProvider.getType(expression, myTypeEvalContext)), myTypeEvalContext)
.allTypeParameters
.any { typeParam -> typeParam.declarationElement !is PyTypeParameter }
if (defaultOutOfScope) {
registerProblem(expression, PyPsiBundle.message("INSP.type.hints.default.type.is.out.of.scope", parent.name))
}
}
}
private fun checkNameIsTheSameAsTarget(argument: PyExpression?, target: PyExpression?,
@InspectionMessage notStringLiteralMessage: String,
@InspectionMessage notEqualMessage: String) {
@@ -793,35 +872,57 @@ class PyTypeHintsInspection : PyInspection() {
val parameters = (index as? PyTupleExpression)?.elements ?: arrayOf(index)
val genericParameters = mutableSetOf<PsiElement>()
var lastIsDefault = false
var lastIsTypeVarTuple = false
val processedGenerics = mutableSetOf<PyType>()
parameters.forEach {
if (it !is PyReferenceExpression && it !is PyStarExpression && it !is PySubscriptionExpression) {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.type.variables"),
ProblemHighlightType.GENERIC_ERROR)
}
else {
val type = myTypeEvalContext.getType(it)
val expression = if (it is PyStarExpression) it.expression else it
if (type != null) {
if (type is PyTypeVarType || isParamSpecOrConcatenate(it, myTypeEvalContext)) {
if (it is PyReferenceExpression && !genericParameters.addAll(multiFollowAssignmentsChain(it))) {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.unique"),
if (expression != null) {
val type = myTypeEvalContext.getType(expression)
if (type != null) {
if (type is PyTypeVarType || isParamSpecOrConcatenate(it, myTypeEvalContext) || type is PyTypeVarTupleType) {
if (it is PyReferenceExpression && !genericParameters.addAll(multiFollowAssignmentsChain(it))) {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.unique"),
ProblemHighlightType.GENERIC_ERROR)
}
}
else {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.type.variables"),
ProblemHighlightType.GENERIC_ERROR)
}
}
else {
registerProblem(it, PyPsiBundle.message("INSP.type.hints.parameters.to.generic.must.all.be.type.variables"),
ProblemHighlightType.GENERIC_ERROR)
}
if (type is PyTypeParameterType) {
val typeVarDeclaration = type.declarationElement
if (hasDefault(typeVarDeclaration)) {
lastIsDefault = true
} else if (lastIsDefault) {
registerProblem(it,
PyPsiBundle.message("INSP.type.hints.non.default.type.vars.cannot.follow.defaults"),
ProblemHighlightType.GENERIC_ERROR)
if (type is PyTypeParameterType) {
val typeVarDeclaration = type.declarationElement
val defaultType = type.defaultType
if (hasDefault(typeVarDeclaration)) {
lastIsDefault = true
if (lastIsTypeVarTuple && type is PyTypeVarType) {
registerProblem(it,
PyPsiBundle.message("INSP.type.hints.default.type.var.cannot.follow.type.var.tuple"),
ProblemHighlightType.GENERIC_ERROR)
}
val genericTypes = PyTypeChecker.collectGenerics(Ref.deref(defaultType), myTypeEvalContext)
val defaultOutOfScope = genericTypes.allTypeParameters.firstOrNull { typeVar -> typeVar !in processedGenerics }
if (defaultOutOfScope != null) {
registerProblem(it,
PyPsiBundle.message("INSP.type.hints.default.type.refers.to.type.var.out.of.scope", defaultOutOfScope.name))
}
}
else if (lastIsDefault) {
registerProblem(it,
PyPsiBundle.message("INSP.type.hints.non.default.type.vars.cannot.follow.defaults"),
ProblemHighlightType.GENERIC_ERROR)
}
processedGenerics.add(type)
}
lastIsTypeVarTuple = type is PyTypeVarTupleType
}
}
}
@@ -839,6 +940,9 @@ class PyTypeHintsInspection : PyInspection() {
}
}
}
if (declarationElement is PyTypeParameter) {
return declarationElement.defaultExpressionText != null
}
return false
}

View File

@@ -141,6 +141,17 @@ public class PyNewStyleGenericSyntaxInspectionTest extends PyInspectionTestCase
"""));
}
// PY-75759
public void testTypeVarCannotFollowTypeVarTuple() {
runWithLanguageLevel(LanguageLevel.PYTHON312,
() -> doTestByText("""
class ClassA[*Ts, <error descr="TypeVar with a default value cannot follow TypeVarTuple">T = int</error>]: ...
class ClassB[*Ts = *tuple[int], <error descr="TypeVar with a default value cannot follow TypeVarTuple">T = int</error>]: ...
class ClassC[*Ts, **P = [float, bool]]: ...
class ClassD[*Ts, **P]: ...
"""));
}
@Override
protected @NotNull Class<? extends PyInspection> getInspectionClass() {

View File

@@ -1630,6 +1630,158 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase {
""");
}
// PY-75759
public void testTypeVarDefaultsScoping() {
doTestByText("""
from typing import TypeVar, Generic
S1 = TypeVar("S1")
S2 = TypeVar("S2", default=S1)
StepT = TypeVar("StepT", default=int | None)
StartT = TypeVar("StartT", default="StopT")
StopT = TypeVar("StopT", default=int)
class slice(Generic[<warning descr="Type parameter has a default type that refers to one or more type variables that are out of scope">StartT</warning>, StopT, StepT]): ...
class slice2(Generic[StopT, StartT, StepT]): ...
class Foo3(Generic[S1]):
class Bar2(Generic[<warning descr="Type parameter has a default type that refers to one or more type variables that are out of scope">S2</warning>]): ...
""");
}
// PY-75759
public void testTypeVarAllowedDefaultValues() {
doTestByText("""
from typing import TypeVar, Generic
T = TypeVar("T", default=<warning descr="Default type must be a type expression">3</warning>)
T1 = TypeVar("T1", default=<warning descr="Default type must be a type expression">True</warning>)
T3 = TypeVar("T3", default="NormalT")
NormalT = TypeVar("NormalT")
T4 = TypeVar("T4", default=NormalT)
T5 = TypeVar("T5", default=list)
class Clazz: ...
T6 = TypeVar("T6", default=Clazz)
""");
}
// PY-75759
public void testNewStyleTypeVarAllowedDefaultValues() {
doTestByText("""
from typing import TypeVar, Generic, ParamSpec, TypeVarTuple
T1 = TypeVar("T1")
Ts1 = TypeVarTuple("Ts1")
P1 = ParamSpec("P1")
class Clazz[T = int]: ...
class Clazz[T = dict[int, str]]: ...
class Clazz[T, T1 = T]: ...
class Clazz[T = <warning descr="Default type must be a type expression">1</warning>]: ...
class Clazz[T = <warning descr="Default type must be a type expression">True</warning>]: ...
class Clazz[T = <warning descr="Default type of type parameter 'T' refers to a type variable that is out of scope">T1</warning>]: ...
class Clazz[T = <warning descr="Default type of type parameter 'T' refers to a type variable that is out of scope">Ts1</warning>]: ...
class Clazz[T = <warning descr="Default type of type parameter 'T' refers to a type variable that is out of scope">P1</warning>]: ...
class Clazz[T = <warning descr="Default type of type parameter 'T' refers to a type variable that is out of scope">list[T1]</warning>]: ...
""");
}
// PY-75759
public void testParamSpecAllowedDefaultValues() {
doTestByText("""
from typing import ParamSpec, TypeVar
T = TypeVar(<warning descr="The argument to 'TypeVar()' must be a string equal to the variable name to which it is assigned">"T1"</warning>)
P = ParamSpec(<warning descr="The argument to 'ParamSpec()' must be a string equal to the variable name to which it is assigned">"P1"</warning>)
P1 = ParamSpec("P1", default=[])
P2 = ParamSpec("P2", default=[int, str, None, int | None])
P3 = ParamSpec("P3", default=[int, T])
P4 = ParamSpec("P4", default=[int])
P5 = ParamSpec("P5", default=...)
P6 = ParamSpec("P6", default=<warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types">int</warning>)
P7 = ParamSpec("P7", default=<warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types">3</warning>)
P8 = ParamSpec("P8", default=<warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types">(1, int)</warning>)
P9 = ParamSpec("P9", default=P)
P10 = ParamSpec("P10", default=[<warning descr="Default type must be a type expression">1</warning>, <warning descr="Default type must be a type expression">2</warning>])
""");
}
// PY-75759
public void testNewStyleParamSpecAllowedDefaultValues() {
doTestByText("""
from typing import TypeVar, Generic, ParamSpec, TypeVarTuple
T1 = TypeVar("T1")
Ts1 = TypeVarTuple("Ts1")
P1 = ParamSpec("P1")
class Clazz[**P = []]: ...
class Clazz[**P = [int]]: ...
class Clazz[**P = [int, str]]: ...
class Clazz[**P = [int, <warning descr="Default type must be a type expression">3</warning>]]: ...
class Clazz[**P = [int, <warning descr="Default type must be a type expression">True</warning>]]: ...
class Clazz[**P = <warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types">True</warning>]: ...
class Clazz[**P = <warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types"><warning descr="Default type of type parameter 'P' refers to a type variable that is out of scope">T1</warning></warning>]: ...
class Clazz[**P = [<warning descr="Default type of type parameter 'P' refers to a type variable that is out of scope">T1</warning>]]: ...
class Clazz[**P = <warning descr="Default type of type parameter 'P' refers to a type variable that is out of scope">P1</warning>]: ...
class Clazz[**P = <warning descr="Default type of ParamSpec must be a ParamSpec type or a list of types"><warning descr="Default type of type parameter 'P' refers to a type variable that is out of scope">Ts1</warning></warning>]: ...
class Clazz[**P = [int, <warning descr="Default type of type parameter 'P' refers to a type variable that is out of scope">list[T1]</warning>]]: ...
""");
}
// PY-75759
public void testTypeVarTupleAllowedDefaultValues() {
doTestByText("""
from typing import TypeVarTuple, Unpack, TypeVar
T = TypeVar("T")
Ts0 = TypeVarTuple("Ts0")
Ts1 = TypeVarTuple("Ts1", default=Unpack[tuple[int]])
Ts2 = TypeVarTuple("Ts2", default=<warning descr="Default type of TypeVarTuple must be unpacked">tuple[int]</warning>)
Ts3 = TypeVarTuple("Ts3", default=<warning descr="Default type of TypeVarTuple must be unpacked">int</warning>)
Ts4 = TypeVarTuple("Ts4", default=Unpack[Ts0])
Ts5 = TypeVarTuple("Ts5", default=<warning descr="Default type of TypeVarTuple must be unpacked">Ts0</warning>)
Ts6 = TypeVarTuple("Ts6", default=Unpack[tuple[int, ...]])
Ts7 = TypeVarTuple("Ts7", default=Unpack[tuple[T, T]])
""");
}
// PY-75759
public void testNewStyleTypeVarTupleAllowedDefaultValues() {
doTestByText("""
from typing import TypeVar, Generic, ParamSpec, TypeVarTuple, Unpack
T1 = TypeVar("T1")
Ts1 = TypeVarTuple("Ts1")
P1 = ParamSpec("P1")
class Clazz[*Ts = <warning descr="Default type of type parameter 'Ts' refers to a type variable that is out of scope">Unpack[tuple[int, T1]]</warning>]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">1</warning>]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">True</warning>]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">tuple[int]</warning>]: ...
class Clazz[*Ts = *tuple[int]]: ...
class Clazz[*Ts = Unpack[tuple[int]]]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">T1</warning>]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">Ts1</warning>]: ...
class Clazz[*Ts = <warning descr="Default type of TypeVarTuple must be unpacked">P1</warning>]: ...
class Clazz[*Ts = Unpack[tuple[int, ...]]]: ...
""");
}
// PY-75759
public void testTypeVarCannotFollowTypeVarTuple() {
doTestByText("""
from typing import TypeVar, Generic, ParamSpec, TypeVarTuple, Unpack
T = TypeVar("T", default = int)
Ts = TypeVarTuple("Ts")
TsDef = TypeVarTuple("TsDef", default = Unpack[tuple[int, int]])
P = ParamSpec("P", default = [str, bool])
class Clazz(Generic[Ts, <error descr="TypeVar with a default value cannot follow TypeVarTuple">T</error>]): ...
class Clazz1(Generic[TsDef, <error descr="TypeVar with a default value cannot follow TypeVarTuple">T</error>]): ...
class Clazz2(Generic[TsDef, P]): ...
class Clazz3(Generic[Ts, P]): ...
""");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {