PY-80627 Overloaded abstract methods and methods within Protocols do not require an implementation

GitOrigin-RevId: a53889a8d6f4ed9ad7fde1e7fa02025f88e292a3
This commit is contained in:
Petr
2025-05-17 13:45:32 +00:00
committed by intellij-monorepo-bot
parent 6f9f976018
commit 8abfc73b15
4 changed files with 71 additions and 25 deletions
@@ -17,8 +17,8 @@ import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PythonUiService;
import com.jetbrains.python.codeInsight.typing.PyProtocolsKt;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyClassImpl;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.refactoring.PyPsiRefactoringUtil;
@@ -49,7 +49,7 @@ public final class PyAbstractClassInspection extends PyInspection {
if (node.getCallee() instanceof PyReferenceExpression calleeReferenceExpression) {
QualifiedResolveResult resolveResult = calleeReferenceExpression.followAssignmentsChain(getResolveContext());
if (resolveResult.getElement() instanceof PyClass pyClass) {
if (canHaveAbstractMethods(pyClass)) {
if (PyClassImpl.canHaveAbstractMethods(pyClass, myTypeEvalContext)) {
boolean hasAbstractMethod =
ContainerUtil.exists(pyClass.getMethods(), method -> PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext));
if (hasAbstractMethod || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
@@ -84,7 +84,7 @@ public final class PyAbstractClassInspection extends PyInspection {
}
}
if (!canHaveAbstractMethods(pyClass)) {
if (!PyClassImpl.canHaveAbstractMethods(pyClass, myTypeEvalContext)) {
for (PyDecorator decorator : abstractDecorators) {
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
addMakeClassAbstractFixes(pyClass, quickFixes);
@@ -114,17 +114,6 @@ public final class PyAbstractClassInspection extends PyInspection {
}
}
private boolean canHaveAbstractMethods(@NotNull PyClass pyClass) {
PyClassLikeType metaClassType = pyClass.getMetaClassType(true, myTypeEvalContext);
if (metaClassType != null && PyNames.ABC_META.equals(metaClassType.getClassQName())) {
return true;
}
return pyClass.getAncestorTypes(myTypeEvalContext).stream()
.filter(Objects::nonNull)
.map(PyClassLikeType::getClassQName)
.anyMatch(qName -> PyTypingTypeProvider.PROTOCOL.equals(qName) || PyTypingTypeProvider.PROTOCOL_EXT.equals(qName));
}
private boolean isAbstract(@NotNull PyClass pyClass) {
final PyClassLikeType metaClass = pyClass.getMetaClassType(false, myTypeEvalContext);
if (metaClass != null && PyNames.ABC_META_CLASS.equals(metaClass.getName())) {
@@ -8,10 +8,13 @@ import com.intellij.util.Processor
import com.intellij.util.containers.SortedList
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner
import com.jetbrains.python.codeInsight.typing.isProtocol
import com.jetbrains.python.psi.PyClass
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyKnownDecoratorUtil
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.psi.impl.PyClassImpl
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.pyi.PyiFile
import com.jetbrains.python.pyi.PyiUtil
@@ -48,28 +51,38 @@ class PyOverloadsInspection : PyInspection() {
}
private fun processSameNameFunctions(owner: ScopeOwner, functions: List<PyFunction>) {
if (functions.find { PyiUtil.isOverload(it, myTypeEvalContext) } == null) return
val (overloads, implementations) = functions.partition { PyiUtil.isOverload(it, myTypeEvalContext) }
val implementation = functions.lastOrNull { !PyiUtil.isOverload(it, myTypeEvalContext) }
if (overloads.isEmpty()) return
if (implementation == null) {
registerProblem(functions.first().nameIdentifier, if (owner is PyClass) {
PyPsiBundle.message("INSP.overloads.series.overload.decorated.methods.should.always.be.followed.by.implementation")
var requiresImplementation = true
if (owner is PyClass) {
if (isProtocol(owner, myTypeEvalContext)) {
requiresImplementation = false
}
else {
PyPsiBundle.message("INSP.overloads.series.overload.decorated.functions.should.always.be.followed.by.implementation")
})
if (PyClassImpl.canHaveAbstractMethods(owner, myTypeEvalContext)) {
if (overloads.all { PyKnownDecoratorUtil.hasAbstractDecorator(it, myTypeEvalContext) }) {
requiresImplementation = false
}
}
}
}
else {
if (implementation != functions.last()) {
registerProblem(functions.last().nameIdentifier, if (owner is PyClass) {
val implementation = implementations.lastOrNull()
if (requiresImplementation) {
if (implementation !== functions.last()) {
val problemElement = if (implementation == null) functions.first() else functions.last()
registerProblem(problemElement.nameIdentifier, if (owner is PyClass) {
PyPsiBundle.message("INSP.overloads.series.overload.decorated.methods.should.always.be.followed.by.implementation")
}
else {
PyPsiBundle.message("INSP.overloads.series.overload.decorated.functions.should.always.be.followed.by.implementation")
})
}
}
if (implementation != null) {
functions
.asSequence()
.filter { isIncompatibleOverload(implementation, it) }
@@ -47,6 +47,7 @@ import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.pyi.PyiUtil;
import com.jetbrains.python.toolbox.Maybe;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -1770,6 +1771,18 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
return getStubOrPsiChild(PyStubElementTypes.DECORATOR_LIST);
}
@ApiStatus.Experimental
public static boolean canHaveAbstractMethods(@NotNull PyClass pyClass, @NotNull TypeEvalContext context) {
PyClassLikeType metaClassType = pyClass.getMetaClassType(true, context);
if (metaClassType != null && PyNames.ABC_META.equals(metaClassType.getClassQName())) {
return true;
}
return pyClass.getAncestorTypes(context).stream()
.filter(Objects::nonNull)
.map(PyClassLikeType::getClassQName)
.anyMatch(qName -> PyTypingTypeProvider.PROTOCOL.equals(qName) || PyTypingTypeProvider.PROTOCOL_EXT.equals(qName));
}
private @NotNull TypeEvalContext notNullizeContext(@Nullable TypeEvalContext context) {
return context == null ? TypeEvalContext.codeInsightFallback(getProject()) : context;
}
@@ -1,4 +1,5 @@
from typing import overload
from abc import ABC, abstractmethod
from typing import overload, Protocol
@overload
@@ -27,4 +28,34 @@ class A:
@overload
def foo(self, value: str) -> str:
pass
class P(Protocol):
@overload
def foo(self, x: int) -> int:
pass
@overload
def foo(self, x: str) -> str:
pass
class Abstract(ABC):
@overload
@abstractmethod
def foo(self, x: int) -> int:
pass
@overload
@abstractmethod
def foo(self, x: str) -> str:
pass
@overload
def <warning descr="A series of @overload-decorated methods should always be followed by an implementation that is not @overload-ed">not_abstract</warning>(self, x: int) -> int:
pass
@overload
def not_abstract(self, x: str) -> str:
pass