Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,26 @@
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;
import com.jetbrains.python.psi.PyExpression;
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;

Expand Down Expand Up @@ -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<PyCallable> 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);
Expand Down
18 changes: 18 additions & 0 deletions python/testData/deprecation/deprecatedPropertySetter.py
Original file line number Diff line number Diff line change
@@ -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.<warning descr="this setter is deprecated">value</warning> = 1
16 changes: 16 additions & 0 deletions python/testData/deprecation/deprecatedPropertySetterFromStub.py
Original file line number Diff line number Diff line change
@@ -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.<warning descr="this setter is deprecated in stub">value</warning> = 1
10 changes: 10 additions & 0 deletions python/testData/deprecation/deprecatedPropertySetterFromStub.pyi
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions python/testSrc/com/jetbrains/python/PyDeprecationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down