PY-80627 @staticmethod, @classmethod usage with overloaded methods

GitOrigin-RevId: 3712c0f4dfb70146e3127af02c4fc04de75dcd33
This commit is contained in:
Petr
2025-05-17 13:45:32 +00:00
committed by intellij-monorepo-bot
parent 58cd6bb893
commit 57c773ccb3
4 changed files with 62 additions and 18 deletions
@@ -1049,6 +1049,8 @@ INSP.overloads.this.method.overload.signature.not.compatible.with.implementation
INSP.overloads.this.function.overload.signature.not.compatible.with.implementation=Signature of this @overload-decorated function is not compatible with the implementation
INSP.overloads.at.least.two.overload.decorated.methods.must.be.present=At least two @overload-decorated methods must be present
INSP.overloads.at.least.two.overload.decorated.functions.must.be.present=At least two @overload-decorated functions must be present
INSP.overloads.use.staticmethod.inconsistently=Overloads use @staticmethod inconsistently
INSP.overloads.use.classmethod.inconsistently=Overloads use @classmethod inconsistently
# PyOverridesInspection
INSP.NAME.invalid.usages.of.override.decorator=Invalid usages of @override decorator
@@ -6,24 +6,25 @@ import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.intellij.util.Processor
import com.intellij.util.containers.SortedList
import com.intellij.util.containers.sequenceOfNotNull
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.ast.PyAstFunction
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.*
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
import java.util.EnumSet
class PyOverloadsInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession): PsiElementVisitor = Visitor(
override fun buildVisitor(
holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession,
): PsiElementVisitor = Visitor(
holder,PyInspectionVisitor.getContext(session))
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
@@ -64,6 +65,10 @@ class PyOverloadsInspection : PyInspection() {
})
}
val implementation = implementations.lastOrNull()
checkClassMethodAndStaticMethodConsistency(overloads, implementation)
var requiresImplementation = true
if (owner is PyClass) {
if (isProtocol(owner, myTypeEvalContext)) {
@@ -78,17 +83,14 @@ class PyOverloadsInspection : PyInspection() {
}
}
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 (requiresImplementation && 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) {
@@ -106,6 +108,19 @@ class PyOverloadsInspection : PyInspection() {
}
}
private fun checkClassMethodAndStaticMethodConsistency(overloads: List<PyFunction>, implementation: PyFunction?) {
val modifiers = overloads.mapNotNullTo(EnumSet.noneOf(PyAstFunction.Modifier::class.java)) { it.modifier }
for (function in overloads.asSequence() + sequenceOfNotNull(implementation)) {
val modifier = function.modifier
if (modifiers.contains(PyAstFunction.Modifier.CLASSMETHOD) && modifier != PyAstFunction.Modifier.CLASSMETHOD) {
registerProblem(function.nameIdentifier, PyPsiBundle.message("INSP.overloads.use.classmethod.inconsistently"))
}
if (modifiers.contains(PyAstFunction.Modifier.STATICMETHOD) && modifier != PyAstFunction.Modifier.STATICMETHOD) {
registerProblem(function.nameIdentifier, PyPsiBundle.message("INSP.overloads.use.staticmethod.inconsistently"))
}
}
}
private fun isIncompatibleOverload(implementation: PyFunction, overload: PyFunction): Boolean {
return implementation != overload &&
PyiUtil.isOverload(overload, myTypeEvalContext) &&
@@ -0,0 +1,23 @@
from typing import overload
class A:
@overload
def <warning descr="Overloads use @staticmethod inconsistently">foo</warning>(self, x: int, /): ...
@staticmethod
@overload
def foo(x: str, /): ...
def <warning descr="Overloads use @staticmethod inconsistently">foo</warning>(*args: object):
pass
@classmethod
@overload
def bar(cls, x: int, /): ...
@overload
def <warning descr="Overloads use @classmethod inconsistently">bar</warning>(self, x: str, /): ...
def <warning descr="Overloads use @classmethod inconsistently">bar</warning>(*args: object):
pass
@@ -44,6 +44,10 @@ public class PyOverloadsInspectionTest extends PyInspectionTestCase {
doTest();
}
public void testStaticOrClassMethods() {
doTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {