PY-78878 Missing error: class type parameter parameterizes an outer scope

GitOrigin-RevId: b819ff172605a949156e5bd96d920044b70e1679
This commit is contained in:
Petr
2025-01-31 15:53:28 +01:00
committed by intellij-monorepo-bot
parent d424ea892b
commit f89bcdcabc
5 changed files with 83 additions and 18 deletions

View File

@@ -1169,6 +1169,7 @@ INSP.type.hints.type.var.tuple.must.always.be.unpacked=TypeVarTuple must always
INSP.type.hints.expected.a.type=Expected a type
INSP.type.hints.metaclass.cannot.be.generic=Metaclass cannot be generic
INSP.type.hints.unbound.type.variable=Unbound type variable
INSP.type.hints.some.type.variables.are.used.by.an.outer.scope=Some type variables ({0}) are used by an outer scope
QFIX.remove.function.annotations=Remove function annotations
QFIX.replace.with.target.name=Replace with the target name
QFIX.remove.generic.parameters=Remove generic parameters

View File

@@ -1654,30 +1654,36 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
PyQualifiedNameOwner typeVarDeclaration = context.getTypeAliasStack().pop();
assert typeVarDeclaration instanceof PyTargetExpression;
try {
if (owner instanceof PyClass) {
return StreamEx.of(collectTypeParameters((PyClass)owner, context))
.findFirst(type -> name.equals(type.getName()))
.orElse(null);
final Iterable<PyTypeParameterType> typeParameters;
if (owner instanceof PyClass cls) {
typeParameters = collectTypeParameters(cls, context);
}
else if (owner instanceof PyFunction function) {
return StreamEx.of(function.getParameterList().getParameters())
.select(PyNamedParameter.class)
.map(parameter -> new PyTypingTypeProvider().getParameterType(parameter, function, context))
.append(new PyTypingTypeProvider().getReturnType(function, context))
.map(Ref::deref)
.map(paramType -> PyTypeChecker.collectGenerics(paramType, context.getTypeContext()))
.flatMap(generics -> StreamEx.<PyTypeParameterType>of(generics.getTypeVars())
.append(generics.getParamSpecs())
.append(generics.getTypeVarTuples())
)
.findFirst(type -> name.equals(type.getName()))
.orElse(null);
typeParameters = collectTypeParameters(function, context.getTypeContext());
}
else {
typeParameters = List.of();
}
return ContainerUtil.find(typeParameters, type -> name.equals(type.getName()));
}
finally {
context.getTypeAliasStack().push(typeVarDeclaration);
}
return null;
}
@ApiStatus.Internal
public static @NotNull Iterable<PyTypeParameterType> collectTypeParameters(@NotNull PyFunction function,
@NotNull TypeEvalContext context) {
return StreamEx.of(function.getParameterList().getParameters())
.select(PyNamedParameter.class)
.map(parameter -> new PyTypingTypeProvider().getParameterType(parameter, function, context))
.append(new PyTypingTypeProvider().getReturnType(function, context))
.map(Ref::deref)
.map(paramType -> PyTypeChecker.collectGenerics(paramType, context))
.flatMap(generics -> StreamEx.<PyTypeParameterType>of(generics.getTypeVars())
.append(generics.getParamSpecs())
.append(generics.getTypeVarTuples())
);
}
private static @NotNull PsiElement getStubRetainedTypeHintContext(@NotNull PsiElement typeHintExpression) {

View File

@@ -109,6 +109,7 @@ class PyTypeHintsInspection : PyInspection() {
checkPlainGenericInheritance(superClassExpressions)
checkGenericDuplication(superClassExpressions)
checkGenericCompleteness(node)
checkGenericClassTypeParametersNotUsedByOuterScope(node)
checkMetaClass(node.metaClassExpression)
}
@@ -866,6 +867,40 @@ class PyTypeHintsInspection : PyInspection() {
return Pair(if (seenGeneric) genericTypeVars else null, nonGenericTypeVars)
}
private fun checkGenericClassTypeParametersNotUsedByOuterScope(cls: PyClass) {
fun getTypeParameters(clazz: PyClass): Iterable<PyTypeParameterType> {
val clazzType = PyTypeChecker.findGenericDefinitionType(clazz, myTypeEvalContext) ?: return emptyList()
return clazzType.elementTypes.filterIsInstance<PyTypeParameterType>()
}
val names = getTypeParameters(cls).map { it.name }.toMutableSet()
if (names.isEmpty()) {
return
}
val namesUsedByOuterScopes = mutableListOf<String>()
var scopeOwner: ScopeOwner? = cls
do {
scopeOwner = PsiTreeUtil.getParentOfType(scopeOwner, PyClass::class.java, PyFunction::class.java)
val typeParameters = when (scopeOwner) {
is PyClass -> getTypeParameters(scopeOwner)
is PyFunction -> PyTypingTypeProvider.collectTypeParameters(scopeOwner, myTypeEvalContext)
else -> break
}
for (typeParameter in typeParameters) {
val name = typeParameter.name
if (names.remove(name)) {
namesUsedByOuterScopes.add(name)
}
}
}
while (true)
if (namesUsedByOuterScopes.isNotEmpty()) {
registerProblem(cls.nameIdentifier, PyPsiBundle.message("INSP.type.hints.some.type.variables.are.used.by.an.outer.scope",
namesUsedByOuterScopes.joinToString(", ")))
}
}
private fun checkParameters(node: PySubscriptionExpression) {
val operand = node.operand as? PyReferenceExpression ?: return
val index = node.indexExpression ?: return

View File

@@ -39,7 +39,6 @@ generics_paramspec_basic.py
generics_paramspec_components.py
generics_paramspec_semantics.py
generics_paramspec_specialization.py
generics_scoping.py
generics_self_advanced.py
generics_self_basic.py
generics_self_usage.py

View File

@@ -304,6 +304,30 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase {
...""");
}
public void testGenericClassCannotUseTypeVariablesFromOuterScope() {
doTestByText("""
from typing import TypeVar, Generic, Iterable
T = TypeVar('T')
S = TypeVar('S')
def a_fun(x: T) -> None:
a_list: list[T] = []
class <warning descr="Some type variables (T) are used by an outer scope">MyGeneric</warning>(Generic[T]):
...
class Outer(Generic[T]):
class <warning descr="Some type variables (T) are used by an outer scope">Bad</warning>(Iterable[T]):
...
class AlsoBad:
x: list[<warning descr="Unbound type variable">T</warning>]
class Inner(Iterable[S]):
...
attr: Inner[T]""");
}
// PY-28249
public void testInstanceAndClassChecksOnAny() {
doTestByText("""