mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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. GitOrigin-RevId: 55cb4dc90c55ddc63991a4c3f6b50b4e34a3b4bd
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f560f56c33
commit
22454fca8f
+5
-3
@@ -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>
|
||||
|
||||
@@ -882,8 +882,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
|
||||
|
||||
+50
-8
@@ -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
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
|
||||
class AbstractClassA(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self): ...
|
||||
|
||||
@abstractmethod
|
||||
def bar(self): ...
|
||||
|
||||
|
||||
a = <warning descr="Cannot instantiate abstract class 'AbstractClassA'">AbstractClassA()</warning>
|
||||
|
||||
|
||||
class <weak_warning descr="Class AbstractClassB must implement all abstract methods">AbstractClassB</weak_warning>(AbstractClassA):
|
||||
...
|
||||
|
||||
|
||||
b = <warning descr="Cannot instantiate abstract class 'AbstractClassB'">AbstractClassB()</warning>
|
||||
|
||||
|
||||
class <weak_warning descr="Class AbstractClassC must implement all abstract methods">AbstractClassC</weak_warning>(AbstractClassB):
|
||||
def foo(self): ...
|
||||
|
||||
|
||||
c = <warning descr="Cannot instantiate abstract class 'AbstractClassC'">AbstractClassC()</warning>
|
||||
|
||||
|
||||
class ConcreteClassC(AbstractClassC):
|
||||
def bar(self): ...
|
||||
|
||||
|
||||
cc = ConcreteClassC()
|
||||
@@ -0,0 +1,16 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
class AbstractClass(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def foo(self):
|
||||
...
|
||||
|
||||
|
||||
def bar(cls: type[AbstractClass]):
|
||||
return cls().foo()
|
||||
|
||||
|
||||
AC = AbstractClass
|
||||
|
||||
<warning descr="Cannot instantiate abstract class 'AbstractClass'">AC()</warning>
|
||||
@@ -0,0 +1,29 @@
|
||||
from abc import ABC, ABCMeta
|
||||
|
||||
|
||||
class AbstractClass1(ABC):
|
||||
def foo(self) -> None: ...
|
||||
|
||||
|
||||
c1 = <weak_warning descr="Cannot instantiate abstract class 'AbstractClass1'">AbstractClass1()</weak_warning>
|
||||
|
||||
|
||||
class ConcreteClass1(AbstractClass1):
|
||||
...
|
||||
|
||||
|
||||
cc1 = ConcreteClass1()
|
||||
|
||||
|
||||
class AbstractClass2(metaclass=ABCMeta):
|
||||
...
|
||||
|
||||
|
||||
c2 = <weak_warning descr="Cannot instantiate abstract class 'AbstractClass2'">AbstractClass2()</weak_warning>
|
||||
|
||||
|
||||
class ConcreteClass2(AbstractClass2):
|
||||
def bar(self) -> None: ...
|
||||
|
||||
|
||||
cc2 = ConcreteClass2()
|
||||
@@ -83,7 +83,7 @@ def test_mp5(mp5: MP5):
|
||||
pass
|
||||
|
||||
|
||||
test_mp5(CMP5())
|
||||
test_mp5(<warning descr="Cannot instantiate abstract class 'CMP5'">CMP5()</warning>)
|
||||
|
||||
|
||||
# ------------------
|
||||
@@ -103,7 +103,7 @@ def test_mp6(mp6: MP6):
|
||||
pass
|
||||
|
||||
|
||||
test_mp6(CMP6())
|
||||
test_mp6(<warning descr="Cannot instantiate abstract class 'CMP6'">CMP6()</warning>)
|
||||
|
||||
|
||||
# ------------------
|
||||
@@ -115,4 +115,6 @@ class MP7(Protocol):
|
||||
|
||||
class PMP7(MP7, Protocol): # ok
|
||||
def bar(self) -> int:
|
||||
pass
|
||||
pass
|
||||
|
||||
<warning descr="Cannot instantiate abstract class 'PMP7'">PMP7()</warning>
|
||||
@@ -55,7 +55,6 @@ protocols_class_objects.py
|
||||
protocols_definition.py
|
||||
protocols_explicit.py
|
||||
protocols_generic.py
|
||||
protocols_merging.py
|
||||
protocols_modules.py
|
||||
protocols_recursive.py
|
||||
protocols_runtime_checkable.py
|
||||
|
||||
@@ -76,6 +76,21 @@ public class PyAbstractClassInspectionTest extends PyInspectionTestCase {
|
||||
doMultiFileTest();
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testInstantiateAbstractClass() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testInstantiateAbstractClass2() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testInstantiateAbstractClass3() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected Class<? extends PyInspection> getInspectionClass() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlin.io.path.name
|
||||
|
||||
private val inspections
|
||||
get() = arrayOf(
|
||||
PyAbstractClassInspection(),
|
||||
PyArgumentListInspection(),
|
||||
PyAssertTypeInspection(),
|
||||
PyCallingNonCallableInspection(),
|
||||
|
||||
Reference in New Issue
Block a user