PY-76859 Report a warning if a class being constructed does not match __init__ method's self parameter type

GitOrigin-RevId: ba64f70594968e4e2981e590d0f0c3d00003f7da
This commit is contained in:
Petr
2025-12-10 18:17:49 +00:00
committed by intellij-monorepo-bot
parent 046cafe8b4
commit 765735660d
2 changed files with 33 additions and 8 deletions
@@ -421,7 +421,7 @@ public class PyTypeCheckerInspection extends PyInspection {
.nonNull()
.toList();
if (!matchedCalleeResultsExist(calleesResults)) {
if (!ContainerUtil.exists(calleesResults, calleeResults -> isMatched(calleeResults))) {
PyTypeCheckerInspectionProblemRegistrar
.registerProblem(this, callSite, getArgumentTypes(calleesResults), calleesResults, myTypeEvalContext);
}
@@ -472,6 +472,22 @@ public class PyTypeCheckerInspection extends PyInspection {
final var receiver = callSite.getReceiver(callableType.getCallable());
final var substitutions = PyTypeChecker.unifyReceiver(receiver, myTypeEvalContext);
// When a constructor call resolves to `__init__` method,
// match the class being constructed against the type of `self` parameter.
if (PyUtil.isInitMethod(callableType.getCallable()) &&
receiver != null &&
myTypeEvalContext.getType(receiver) instanceof PyClassType receiverType &&
receiverType.isDefinition()) {
PyCallableParameter selfParameter = ContainerUtil.getFirstItem(mapping.getImplicitParameters());
if (selfParameter != null) {
final PyType actual = receiverType.toInstance();
final PyType expected = selfParameter.getArgumentType(myTypeEvalContext);
final boolean matched = matchParameterAndArgument(expected, actual, receiver, substitutions);
result.add(new AnalyzeArgumentResult(receiver, expected, substituteGenerics(expected, substitutions), actual, matched));
}
}
final var mappedParameters = mapping.getMappedParameters();
final var regularMappedParameters = getRegularMappedParameters(mappedParameters);
@@ -668,13 +684,11 @@ public class PyTypeCheckerInspection extends PyInspection {
: null;
}
private static boolean matchedCalleeResultsExist(@NotNull List<AnalyzeCalleeResults> calleesResults) {
return ContainerUtil.exists(calleesResults, calleeResults ->
ContainerUtil.all(calleeResults.getResults(), AnalyzeArgumentResult::isMatched) &&
calleeResults.getUnmatchedArguments().isEmpty() &&
calleeResults.getUnmatchedParameters().isEmpty() &&
calleeResults.getUnfilledPositionalVarargs().isEmpty()
);
private static boolean isMatched(@NotNull AnalyzeCalleeResults calleeResults) {
return ContainerUtil.all(calleeResults.getResults(), AnalyzeArgumentResult::isMatched) &&
calleeResults.getUnmatchedArguments().isEmpty() &&
calleeResults.getUnmatchedParameters().isEmpty() &&
calleeResults.getUnfilledPositionalVarargs().isEmpty();
}
private static @NotNull List<PyType> getArgumentTypes(@NotNull List<AnalyzeCalleeResults> calleesResults) {
@@ -1680,6 +1680,17 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase {
subClass.foo(subClass.foo(<warning descr="Expected type 'SubClass' (matched generic type 'Self@MyClass'), got 'MyClass' instead">myClass</warning>))""");
}
public void testSelfParameterType() {
doTestByText("""
class MyClass[T]:
def __init__(self: "MyClass[int]") -> None: ...
MyClass()
MyClass[int]()
<warning descr="Expected type 'MyClass[int]', got 'MyClass[str]' instead">MyClass[str]</warning>()
""");
}
// PY-53104
public void testProtocolSelfClass() {
doTestByText("""