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)
Report error for abstract method in non-abstract class. GitOrigin-RevId: 824d45a310b95ada628d6e607ae1ced6e351d849
This commit is contained in:
committed by
intellij-monorepo-bot
parent
22454fca8f
commit
3060358b39
+14
@@ -38,6 +38,20 @@ class Triangle(Figure):
|
||||
|
||||
def do_triangle(self):
|
||||
pass
|
||||
|
||||
|
||||
Triangle()
|
||||
</code></pre>
|
||||
|
||||
It also warns you if <code>abc.abstractmethod</code> is used in a class whose metaclass is not <code>abc.ABCMeta</code>:
|
||||
<code><pre>
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class MyClass:
|
||||
@abstractmethod # 'MyClass' is not abstract
|
||||
def foo(self):
|
||||
...
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -885,6 +885,7 @@ INSP.method.may.be.static=Method <code>#ref</code> may be 'static'
|
||||
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}''
|
||||
INSP.abstract.class.abstract.methods.are.allowed.in.classes.whose.metaclass.is.abcmeta=Abstract methods are allowed in classes whose metaclass is 'ABCMeta'
|
||||
|
||||
#PyAssignmentToLoopOrWithParameterInspection
|
||||
INSP.NAME.assignment.to.loop.or.with.parameter=Assignments to 'for' loop or 'with' statement parameter
|
||||
|
||||
+66
-29
@@ -24,9 +24,9 @@ import com.jetbrains.python.psi.types.*;
|
||||
import com.jetbrains.python.refactoring.PyPsiRefactoringUtil;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
public final class PyAbstractClassInspection extends PyInspection {
|
||||
|
||||
@@ -54,7 +54,9 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
QualifiedResolveResult resolveResult = calleeReferenceExpression.followAssignmentsChain(getResolveContext());
|
||||
if (resolveResult.getElement() instanceof PyClass pyClass) {
|
||||
if (canHaveAbstractMethods(pyClass)) {
|
||||
if (hasAbstractMethod(pyClass) || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
|
||||
boolean hasAbstractMethod =
|
||||
ContainerUtil.exists(pyClass.getMethods(), method -> PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext));
|
||||
if (hasAbstractMethod || !getAllSuperAbstractMethods(pyClass).isEmpty()) {
|
||||
registerProblem(node, canNotInstantiateAbstractClassMessage(pyClass), ProblemHighlightType.WARNING);
|
||||
}
|
||||
else if (isAbstract(pyClass)) {
|
||||
@@ -67,29 +69,51 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
|
||||
@Override
|
||||
public void visitPyClass(@NotNull PyClass pyClass) {
|
||||
if (isAbstract(pyClass) || hasAbstractMethod(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
|
||||
if (isAbstract(pyClass) || PyProtocolsKt.isProtocol(pyClass, myTypeEvalContext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<PyFunction> toImplement = getAllSuperAbstractMethods(pyClass);
|
||||
List<PyDecorator> abstractDecorators = new ArrayList<>();
|
||||
for (PyFunction method : pyClass.getMethods()) {
|
||||
PyDecoratorList decoratorList = method.getDecoratorList();
|
||||
if (decoratorList == null) continue;
|
||||
|
||||
final ASTNode nameNode = pyClass.getNameNode();
|
||||
if (!toImplement.isEmpty() && nameNode != null) {
|
||||
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
|
||||
|
||||
LocalQuickFix qf = PythonUiService.getInstance().createPyImplementMethodsQuickFix(pyClass, toImplement);
|
||||
if (qf != null) {
|
||||
quickFixes.add(qf);
|
||||
for (PyDecorator decorator : decoratorList.getDecorators()) {
|
||||
boolean isAbstract = ContainerUtil.exists(PyKnownDecoratorUtil.asKnownDecorators(decorator, myTypeEvalContext),
|
||||
PyKnownDecorator::isAbstract);
|
||||
if (isAbstract) {
|
||||
abstractDecorators.add(decorator);
|
||||
}
|
||||
}
|
||||
quickFixes.add(new SetABCMetaAsMetaclassQuickFix());
|
||||
}
|
||||
|
||||
if (LanguageLevel.forElement(pyClass).isPy3K()) {
|
||||
quickFixes.add(new AddABCToSuperclassesQuickFix());
|
||||
if (!canHaveAbstractMethods(pyClass)) {
|
||||
for (PyDecorator decorator : abstractDecorators) {
|
||||
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
|
||||
addMakeClassAbstractFixes(pyClass, quickFixes);
|
||||
registerProblem(decorator,
|
||||
PyPsiBundle.message("INSP.abstract.class.abstract.methods.are.allowed.in.classes.whose.metaclass.is.abcmeta"),
|
||||
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
|
||||
}
|
||||
}
|
||||
|
||||
registerProblem(nameNode.getPsi(),
|
||||
PyPsiBundle.message("INSP.abstract.class.class.must.implement.all.abstract.methods", pyClass.getName()),
|
||||
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
|
||||
if (abstractDecorators.isEmpty()) {
|
||||
final List<PyFunction> toImplement = getAllSuperAbstractMethods(pyClass);
|
||||
|
||||
final ASTNode nameNode = pyClass.getNameNode();
|
||||
if (!toImplement.isEmpty() && nameNode != null) {
|
||||
final SmartList<LocalQuickFix> quickFixes = new SmartList<>();
|
||||
|
||||
LocalQuickFix qf = PythonUiService.getInstance().createPyImplementMethodsQuickFix(pyClass, toImplement);
|
||||
if (qf != null) {
|
||||
quickFixes.add(qf);
|
||||
}
|
||||
addMakeClassAbstractFixes(pyClass, quickFixes);
|
||||
|
||||
registerProblem(nameNode.getPsi(),
|
||||
PyPsiBundle.message("INSP.abstract.class.class.must.implement.all.abstract.methods", pyClass.getName()),
|
||||
quickFixes.toArray(LocalQuickFix.EMPTY_ARRAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,15 +141,6 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasAbstractMethod(@NotNull PyClass pyClass) {
|
||||
for (PyFunction method : pyClass.getMethods()) {
|
||||
if (PyKnownDecoratorUtil.hasAbstractDecorator(method, myTypeEvalContext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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) */
|
||||
@@ -133,6 +148,28 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
function -> PyKnownDecoratorUtil.hasAbstractDecorator(function, myTypeEvalContext));
|
||||
}
|
||||
|
||||
private static void addMakeClassAbstractFixes(@NotNull PsiElement element, @NotNull Collection<LocalQuickFix> fixes) {
|
||||
fixes.add(new SetABCMetaAsMetaclassQuickFix());
|
||||
|
||||
if (LanguageLevel.forElement(element).isPy3K()) {
|
||||
fixes.add(new AddABCToSuperclassesQuickFix());
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable PyClass getPyClass(@NotNull PsiElement element) {
|
||||
// element is a class name node
|
||||
if (element.getParent() instanceof PyClass pyClass) {
|
||||
return pyClass;
|
||||
}
|
||||
// element is a method decorator
|
||||
if (element instanceof PyDecorator decorator) {
|
||||
return Optional.ofNullable(decorator.getTarget())
|
||||
.map(PyFunction::getContainingClass)
|
||||
.orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class AddABCToSuperclassesQuickFix extends PsiUpdateModCommandQuickFix {
|
||||
|
||||
@Override
|
||||
@@ -142,7 +179,7 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
|
||||
@Override
|
||||
public void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
|
||||
final PyClass cls = PyUtil.as(element.getParent(), PyClass.class);
|
||||
final PyClass cls = getPyClass(element);
|
||||
if (cls == null) return;
|
||||
|
||||
final PyClass abcClass = PyPsiFacade.getInstance(project).createClassByQName(PyNames.ABC, cls);
|
||||
@@ -161,7 +198,7 @@ public final class PyAbstractClassInspection extends PyInspection {
|
||||
|
||||
@Override
|
||||
public void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
|
||||
final PyClass cls = PyUtil.as(element.getParent(), PyClass.class);
|
||||
final PyClass cls = getPyClass(element);
|
||||
if (cls == null) return;
|
||||
|
||||
final PyClass abcMetaClass = PyPsiFacade.getInstance(project).createClassByQName(PyNames.ABC_META, cls);
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
|
||||
class A:
|
||||
<weak_warning descr="Abstract methods are allowed in classes whose metaclass is 'ABCMeta'">@abstractmethod</weak_warning>
|
||||
def foo(self): ...
|
||||
|
||||
|
||||
class AbstractClassA(metaclass=ABCMeta):
|
||||
...
|
||||
|
||||
|
||||
class AbstractClassB(AbstractClassA):
|
||||
@abstractmethod
|
||||
def foo(self): ...
|
||||
|
||||
|
||||
class AbstractClassC(AbstractClassB):
|
||||
@abstractmethod
|
||||
def bar(self): ...
|
||||
|
||||
|
||||
class AbstractClassD(AbstractClassC):
|
||||
def foo(self): ...
|
||||
def bar(self): ...
|
||||
@abstractmethod
|
||||
def buz(self): ...
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import abc
|
||||
|
||||
|
||||
class A:
|
||||
@abc.ab<caret>stractmethod
|
||||
def meth(self):
|
||||
...
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import abc
|
||||
from abc import ABC
|
||||
|
||||
|
||||
class A(ABC):
|
||||
@abc.abstractmethod
|
||||
def meth(self):
|
||||
...
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class A:
|
||||
@ab<caret>stractmethod
|
||||
def meth(self):
|
||||
...
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
from abc import abstractmethod, ABCMeta
|
||||
|
||||
|
||||
class A(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def meth(self):
|
||||
...
|
||||
@@ -146,6 +146,15 @@ public class Py3QuickFixTest extends PyTestCase {
|
||||
true);
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testAddABCToSuperclassesCaretAtAbstractMethod() {
|
||||
doInspectionTest("PyAbstractClassInspection/quickFix/AddABCToSuperclassesCaretAtAbstractMethod/main.py",
|
||||
PyAbstractClassInspection.class,
|
||||
"Add '" + PyNames.ABC + "' to superclasses",
|
||||
true,
|
||||
true);
|
||||
}
|
||||
|
||||
// PY-30789
|
||||
public void testSetABCMetaAsMetaclassPy3() {
|
||||
final String[] testFiles = {
|
||||
@@ -169,6 +178,15 @@ public class Py3QuickFixTest extends PyTestCase {
|
||||
true);
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testSetABCMetaAsMetaclassPy3CaretAtAbstractMethod() {
|
||||
doInspectionTest("PyAbstractClassInspection/quickFix/SetABCMetaAsMetaclassPy3CaretAtAbstractMethod/main.py",
|
||||
PyAbstractClassInspection.class,
|
||||
"Set '" + PyNames.ABC_META + "' as metaclass",
|
||||
true,
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNls
|
||||
protected String getTestDataPath() {
|
||||
|
||||
@@ -91,6 +91,11 @@ public class PyAbstractClassInspectionTest extends PyInspectionTestCase {
|
||||
doTest();
|
||||
}
|
||||
|
||||
// PY-12132
|
||||
public void testAbstractMethodInNonAbstractClass() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected Class<? extends PyInspection> getInspectionClass() {
|
||||
|
||||
Reference in New Issue
Block a user