PY-12132 Support ABC classes (pep-3119)

A class containing at least one method declared with `abstractmethod` decorator that hasn’t been overridden yet cannot be instantiated.

Also report instantiation of classes that directly inherit ABC or have metaclass set to ABCMeta.

(cherry picked from commit 55cb4dc90c55ddc63991a4c3f6b50b4e34a3b4bd)

GitOrigin-RevId: b37ea24490dc5ce7dcce87adf21aa6fe31e0e9dc
This commit is contained in:
Petr
2025-04-07 19:52:57 +02:00
committed by intellij-monorepo-bot
parent 8fdd497e22
commit 42dd3f39bf
10 changed files with 156 additions and 16 deletions

View File

@@ -1,7 +1,6 @@
<html>
<body>
<p>Reports cases when not all abstract properties or methods are defined in
a subclass.</p>
<p>Reports invalid definition and usages of abstract classes.</p>
<p><b>Example:</b></p>
<pre><code>
from abc import abstractmethod, ABC
@@ -14,9 +13,12 @@ class Figure(ABC):
pass
class Triangle(Figure):
class Triangle(Figure): # Not all abstract methods are defined in 'Triangle' class
def do_triangle(self):
pass
Triangle() # Cannot instantiate abstract class 'Triangle'
</code></pre>
<p>When the quick-fix is applied, the IDE implements an abstract method for the <code>Triangle</code> class:</p>
<pre><code>

View File

@@ -880,8 +880,9 @@ INSP.NAME.method.may.be.static=Method is not declared static
INSP.method.may.be.static=Method <code>#ref</code> may be 'static'
# PyAbstractClassInspection
INSP.NAME.abstract.class=Class must implement all abstract methods
INSP.NAME.abstract.class=Invalid abstract class definition and usages
INSP.abstract.class.class.must.implement.all.abstract.methods=Class {0} must implement all abstract methods
INSP.abstract.class.cannot.instantiate.abstract.class=Cannot instantiate abstract class ''{0}''
#PyAssignmentToLoopOrWithParameterInspection
INSP.NAME.assignment.to.loop.or.with.parameter=Assignments to 'for' loop or 'with' statement parameter

View File

@@ -3,6 +3,7 @@ package com.jetbrains.python.inspections;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.lang.ASTNode;
import com.intellij.modcommand.ModPsiUpdater;
@@ -16,14 +17,16 @@ 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.types.PyClassLikeType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.refactoring.PyPsiRefactoringUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
public final class PyAbstractClassInspection extends PyInspection {
@@ -36,22 +39,39 @@ public final class PyAbstractClassInspection extends PyInspection {
private static final class Visitor extends PyInspectionVisitor {
private static @Nls @NotNull String canNotInstantiateAbstractClassMessage(@NotNull PyClass pyClass) {
return PyPsiBundle.message("INSP.abstract.class.cannot.instantiate.abstract.class", pyClass.getName());
}
private Visitor(@NotNull ProblemsHolder holder,
@NotNull TypeEvalContext context) {
super(holder, context);
}
@Override
public void visitPyCallExpression(@NotNull PyCallExpression node) {
if (node.getCallee() instanceof PyReferenceExpression calleeReferenceExpression) {
QualifiedResolveResult resolveResult = calleeReferenceExpression.followAssignmentsChain(getResolveContext());
if (resolveResult.getElement() instanceof PyClass pyClass) {
if (canHaveAbstractMethods(pyClass)) {
if (hasAbstractMethod(pyClass) || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
registerProblem(node, canNotInstantiateAbstractClassMessage(pyClass), ProblemHighlightType.WARNING);
}
else if (isAbstract(pyClass)) {
registerProblem(node, canNotInstantiateAbstractClassMessage(pyClass));
}
}
}
}
}
@Override
public void visitPyClass(@NotNull PyClass pyClass) {
if (isAbstract(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
if (isAbstract(pyClass) || hasAbstractMethod(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
return;
}
/* Do not report problem if class contains only methods that raise NotImplementedError without any abc.* decorators
but keep ability to implement them via quickfix (see PY-38680) */
final List<PyFunction> toImplement = ContainerUtil
.filter(PyPsiRefactoringUtil.getAllSuperAbstractMethods(pyClass, myTypeEvalContext),
function -> PyKnownDecoratorUtil.hasAbstractDecorator(function, myTypeEvalContext));
final List<PyFunction> toImplement = getAllSuperAbstractMethods(pyClass);
final ASTNode nameNode = pyClass.getNameNode();
if (!toImplement.isEmpty() && nameNode != null) {
@@ -73,6 +93,17 @@ 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())) {
@@ -83,6 +114,10 @@ public final class PyAbstractClassInspection extends PyInspection {
return true;
}
}
return false;
}
private boolean hasAbstractMethod(@NotNull PyClass pyClass) {
for (PyFunction method : pyClass.getMethods()) {
if (PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext)) {
return true;
@@ -91,6 +126,13 @@ public final class PyAbstractClassInspection extends PyInspection {
return false;
}
private @NotNull List<PyFunction> getAllSuperAbstractMethods(@NotNull PyClass pyClass) {
/* Do not report problem if class contains only methods that raise NotImplementedError without any abc.* decorators
but keep ability to implement them via quickfix (see PY-38680) */
return ContainerUtil.filter(PyPsiRefactoringUtil.getAllSuperAbstractMethods(pyClass, myTypeEvalContext),
function -> PyKnownDecoratorUtil.hasAbstractDecorator(function, myTypeEvalContext));
}
private static class AddABCToSuperclassesQuickFix extends PsiUpdateModCommandQuickFix {
@Override