PY-34368: False warning "This decorator will not receive a callable it may expect" when @classmethod is not the last applied

- improve inspection to respect unaffected decorators wrt. classmethod/staticmethod
- improve inspection message
- add tests

GitOrigin-RevId: 0eb431ae1aa2c40fa9056f7032936bb05bf8c32b
This commit is contained in:
Marcus Mews
2025-06-21 11:11:50 +00:00
committed by intellij-monorepo-bot
parent 836930bab0
commit f35ff1b00a
4 changed files with 112 additions and 27 deletions
@@ -587,7 +587,7 @@ INSP.first.param.must.not.be.tuple=First parameter of a non-static method must n
# PyNestedDecoratorsInspection
INSP.NAME.nested.decorators=Problematic nesting of decorators
INSP.decorator.receives.unexpected.builtin=This decorator will not receive a callable it may expect; the built-in decorator returns a special object
INSP.decorator.receives.unexpected.builtin=This decorator will not receive the callable it may expect; the previously called decorator ''{0}'' returns a special object
# PyRedeclarationInspection
INSP.NAME.redeclaration=Redeclared names without usages
@@ -8,13 +8,14 @@ import com.intellij.psi.PsiElementVisitor;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.inspections.quickfix.RemoveDecoratorQuickFix;
import com.jetbrains.python.psi.PyDecorator;
import com.jetbrains.python.psi.PyDecoratorList;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
/**
* Checks nested decorators, especially whatever comes after @classmethod.
* <br/>
@@ -29,29 +30,89 @@ public final class PyNestedDecoratorsInspection extends PyInspection {
}
public static class Visitor extends PyInspectionVisitor {
private static final Set<String> TRANSFORMING_DECORATORS = Set.of(PyNames.CLASSMETHOD, PyNames.STATICMETHOD);
private static final Set<String> UNAFFECTED_DECORATORS = Set.of(
"typing.final", "typing.no_type_check", "typing.overload", "typing.override", "typing.type_check_only",
"typing_extensions.final", "typing_extensions.no_type_check", "typing_extensions.overload", "typing_extensions.override", "typing_extensions.type_check_only",
"functools.singledispatchmethod",
"pydantic.functional_validators.field_validator", "pydantic.functional_validators.model_validator", "pydantic.class_validators.validator",
"django.views.decorators.cache.cache_page",
"tenacity.retry"
);
public Visitor(@Nullable ProblemsHolder holder, @NotNull TypeEvalContext context) {
super(holder, context);
}
/**
* Marks decorators that are applied before a transforming decorator and might be affected by this.
* A transforming decorator is one that returns other than a function.
* A decorator might be affected if it is not excluded explicitly here.
*
* <pre>
* \@mydeco # < -- warning here
* \@classmethod
* def foo(cls):
* ...
* </pre>
*/
@Override
public void visitPyFunction(final @NotNull PyFunction node) {
PyDecoratorList decolist = node.getDecoratorList();
if (decolist != null) {
PyDecorator[] decos = decolist.getDecorators();
if (decos.length > 1) {
for (int i = decos.length - 1; i >= 1; i -= 1) {
PyDecorator deco = decos[i];
String deconame = deco.getName();
if ((PyNames.CLASSMETHOD.equals(deconame) || PyNames.STATICMETHOD.equals(deconame)) && deco.isBuiltin()) {
registerProblem(
decos[i-1],
PyPsiBundle.message("INSP.decorator.receives.unexpected.builtin"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, null, new RemoveDecoratorQuickFix()
);
}
if (decolist == null) {
return;
}
PyDecorator[] decos = decolist.getDecorators();
if (decos.length < 2) {
return;
}
for (int i = decos.length - 1; i >= 1; i -= 1) { // start at the innermost
PyDecorator decoInner = decos[i];
String decoInnerName = decoInner.getName();
boolean isTransforming = TRANSFORMING_DECORATORS.contains(decoInnerName) && decoInner.isBuiltin();
if (!isTransforming) {
continue;
}
for (int j = i - 1; j >= 0; j -= 1) {
PyDecorator decoOuter = decos[j];
boolean maybeAffected = !isUnaffectedDecorator(decoOuter);
if (maybeAffected) {
registerProblem(
decoOuter,
PyPsiBundle.message("INSP.decorator.receives.unexpected.builtin", decoInnerName),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, null, new RemoveDecoratorQuickFix()
);
return;
}
}
}
}
private boolean isUnaffectedDecorator(@NotNull PyDecorator decorator) {
List<@NotNull PyCallable> pyCallables = decorator.multiResolveCalleeFunction(getResolveContext());
for (PyCallable callable : pyCallables) {
String decoOuterName = getQualifiedName(callable);
if (UNAFFECTED_DECORATORS.contains(decoOuterName)) {
return true;
}
}
return false;
}
private static @Nullable String getQualifiedName(PyCallable callable) {
String decoOuterName = null;
if (callable instanceof PyFunction pyFunction) {
PyClass constrClass = PyUtil.turnConstructorIntoClass(pyFunction);
if (constrClass != null) {
decoOuterName = constrClass.getQualifiedName();
}
}
if (decoOuterName == null) {
decoOuterName = callable.getQualifiedName();
}
return decoOuterName;
}
}
}
@@ -2,27 +2,32 @@
<problems>
<problem>
<file>decorated.py</file>
<line>25</line>
<description>This decorator will not receive a callable it may expect; the built-in decorator returns a special object</description>
<line>28</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'classmethod' returns a special object</description>
</problem>
<problem>
<file>decorated.py</file>
<line>30</line>
<description>This decorator will not receive a callable it may expect; the built-in decorator returns a special object</description>
<line>33</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'staticmethod' returns a special object</description>
</problem>
<problem>
<file>decorated.py</file>
<line>35</line>
<description>This decorator will not receive a callable it may expect; the built-in decorator returns a special object</description>
<line>38</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'staticmethod' returns a special object</description>
</problem>
<problem>
<file>decorated.py</file>
<line>40</line>
<description>This decorator will not receive a callable it may expect; the built-in decorator returns a special object</description>
<line>43</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'classmethod' returns a special object</description>
</problem>
<problem>
<file>decorated.py</file>
<line>47</line>
<description>This decorator will not receive a callable it may expect; the built-in decorator returns a special object</description>
<line>50</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'classmethod' returns a special object</description>
</problem>
<problem>
<file>decorated.py</file>
<line>61</line>
<description>This decorator will not receive the callable it may expect; the previously called decorator 'classmethod' returns a special object</description>
</problem>
</problems>
@@ -1,3 +1,6 @@
from typing import overload, final
def innocent(f):
"A transparent deco"
print("I'm innocent!")
@@ -49,3 +52,19 @@ class A(object):
@innocent
def f2(cls):
pass
@overload # nothing
@classmethod
def g1(cls):
pass
@innocent # warn
@overload # nothing
@classmethod
def g2(cls):
pass
@final # nothing
@classmethod
def g3(cls):
pass