diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDeprecationInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDeprecationInspection.java index 7e756ca67b3bc..72e9041e4c64d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDeprecationInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyDeprecationInspection.java @@ -23,7 +23,11 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.Property; import com.jetbrains.python.psi.PyBinaryExpression; +import com.jetbrains.python.psi.PyCallable; +import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyDeprecatable; import com.jetbrains.python.psi.PyElement; import com.jetbrains.python.psi.PyExceptPart; @@ -31,10 +35,14 @@ import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.psi.PyFromImportStatement; import com.jetbrains.python.psi.PyReferenceExpression; +import com.jetbrains.python.psi.PyTargetExpression; import com.jetbrains.python.psi.PyUtil; +import com.jetbrains.python.psi.types.PyClassType; +import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.pyi.PyiFile; import com.jetbrains.python.pyi.PyiUtil; +import com.jetbrains.python.toolbox.Maybe; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -98,6 +106,48 @@ else if (resolveResult instanceof PyFile) { } } + @Override + public void visitPyTargetExpression(@NotNull PyTargetExpression node) { + final PyExpression qualifier = node.getQualifier(); + if (qualifier == null) return; + + final String name = node.getName(); + if (name == null) return; + + final PyType type = myTypeEvalContext.getType(qualifier); + if (!(type instanceof PyClassType classType)) return; + + final PyClass cls = classType.getPyClass(); + final Property property = cls.findProperty(name, true, myTypeEvalContext); + if (property == null) return; + + final Maybe accessor = property.getByDirection(AccessDirection.WRITE); + if (!accessor.isDefined() || accessor.value() == null) return; + + final PyCallable callable = accessor.value(); + if (!(callable instanceof PyDeprecatable deprecatable)) return; + + @NlsSafe String deprecationMessage = deprecatable.getDeprecationMessage(); + + if (deprecationMessage == null && !(callable.getContainingFile() instanceof PyiFile)) { + PsiElement classStub = PyiUtil.getPythonStub(cls); + if (classStub instanceof PyClass stubClass) { + Property stubProperty = stubClass.findProperty(name, true, myTypeEvalContext); + if (stubProperty != null) { + PyCallable stubSetter = stubProperty.getByDirection(AccessDirection.WRITE).valueOrNull(); + if (stubSetter instanceof PyDeprecatable stubDeprecatable) { + deprecationMessage = stubDeprecatable.getDeprecationMessage(); + } + } + } + } + + if (deprecationMessage != null) { + PsiElement nameIdentifier = node.getNameIdentifier(); + registerProblem(nameIdentifier == null ? node : nameIdentifier, deprecationMessage, ProblemHighlightType.LIKE_DEPRECATED); + } + } + private @Nullable PyElement resolve(@NotNull PyReferenceExpression node) { final PyElement resolve = PyUtil.as(node.getReference(getResolveContext()).resolve(), PyElement.class); return resolve == null ? null : PyiUtil.getOriginalElementOrLeaveAsIs(resolve, PyElement.class); diff --git a/python/testData/deprecation/deprecatedPropertySetter.py b/python/testData/deprecation/deprecatedPropertySetter.py new file mode 100644 index 0000000000000..6e7ec49981a01 --- /dev/null +++ b/python/testData/deprecation/deprecatedPropertySetter.py @@ -0,0 +1,18 @@ +# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +class Foo: + def __init__(self): + self._value = None + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value): + import warnings + warnings.warn("this setter is deprecated", DeprecationWarning, 2) + self._value = new_value + +foo = Foo() +foo.value = 1 diff --git a/python/testData/deprecation/deprecatedPropertySetterFromStub.py b/python/testData/deprecation/deprecatedPropertySetterFromStub.py new file mode 100644 index 0000000000000..fdde10d53520b --- /dev/null +++ b/python/testData/deprecation/deprecatedPropertySetterFromStub.py @@ -0,0 +1,16 @@ +# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +class Foo: + def __init__(self): + self._value = None + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value): + self._value = new_value + +foo = Foo() +foo.value = 1 diff --git a/python/testData/deprecation/deprecatedPropertySetterFromStub.pyi b/python/testData/deprecation/deprecatedPropertySetterFromStub.pyi new file mode 100644 index 0000000000000..59bc5d5121fe6 --- /dev/null +++ b/python/testData/deprecation/deprecatedPropertySetterFromStub.pyi @@ -0,0 +1,10 @@ +# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +class Foo: + @property + def value(self): ... + + @value.setter + def value(self, new_value): + import warnings + warnings.warn("this setter is deprecated in stub", DeprecationWarning, 2) diff --git a/python/testSrc/com/jetbrains/python/PyDeprecationTest.java b/python/testSrc/com/jetbrains/python/PyDeprecationTest.java index 9430c79c132b7..61ebb0d92c6f5 100644 --- a/python/testSrc/com/jetbrains/python/PyDeprecationTest.java +++ b/python/testSrc/com/jetbrains/python/PyDeprecationTest.java @@ -57,6 +57,18 @@ public void testDeprecatedProperty() { myFixture.checkHighlighting(true, false, false); } + public void testDeprecatedPropertySetter() { + myFixture.enableInspections(PyDeprecationInspection.class); + myFixture.configureByFile("deprecation/deprecatedPropertySetter.py"); + myFixture.checkHighlighting(true, false, false); + } + + public void testDeprecatedPropertySetterFromStub() { + myFixture.enableInspections(PyDeprecationInspection.class); + myFixture.configureByFiles("deprecation/deprecatedPropertySetterFromStub.py", "deprecation/deprecatedPropertySetterFromStub.pyi"); + myFixture.checkHighlighting(true, false, false); + } + public void testDeprecatedImport() { myFixture.enableInspections(PyDeprecationInspection.class); myFixture.configureByFiles("deprecation/deprecatedImport.py", "deprecation/deprecatedModule.py");