diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAbstractClassInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAbstractClassInspection.java index c6da5b6e922a..89ad646a1298 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAbstractClassInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAbstractClassInspection.java @@ -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 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())) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt index 1ba3abc82ebf..c0601732a70b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt @@ -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) { - 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) } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java index c185f7992a78..cbd0b8f76f9d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -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 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; } diff --git a/python/testData/inspections/PyOverloadsInspection/noImplementation.py b/python/testData/inspections/PyOverloadsInspection/noImplementation.py index 4c95d5774175..2877bbe3f6f5 100644 --- a/python/testData/inspections/PyOverloadsInspection/noImplementation.py +++ b/python/testData/inspections/PyOverloadsInspection/noImplementation.py @@ -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 not_abstract(self, x: int) -> int: + pass + + @overload + def not_abstract(self, x: str) -> str: pass \ No newline at end of file