From 223a91c3ace324e274fd1c3ea8e51ccf7bc6848b Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 5 Jan 2026 12:38:38 +0200 Subject: [PATCH] PY-85030 When constructing a structural type, make sure attribute qualifiers refer to the original parameter on all paths We already had protection against reassignment to a parameter name, but it didn't take into account that we also skip conditional branches and loops to collect only definitely accessed attributes of an object, so if reassignments took place under conditions, even in every branch, they didn't prevent us from collecting subsequently accessed attributes. GitOrigin-RevId: d41b8f4c3dbce6643d915923172d32a8e4e6466c --- .../python/psi/impl/PyNamedParameterImpl.java | 7 ++-- .../unionContainingUnknownType.py | 2 +- .../com/jetbrains/python/Py3TypeTest.java | 11 +++++++ .../Py3TypeCheckerInspectionTest.java | 32 +++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java index 68aef3c6cdbb..8613eab64a96 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java @@ -271,6 +271,7 @@ public class PyNamedParameterImpl extends PyBaseElementImpl parameterWasReassigned = Ref.create(false); final Ref noneComparison = Ref.create(false); + final PyResolveContext resolveContext = PyResolveContext.defaultContext(context); if (owner != null && name != null) { owner.accept(new PyRecursiveElementVisitor() { @@ -378,8 +379,10 @@ public class PyNamedParameterImpl extends PyBaseElementImpl false") private boolean isReferenceToParameter(@Nullable PsiElement element) { if (element == null) return false; - final PsiReference reference = element.getReference(); - return reference != null && reference.isReferenceTo(PyNamedParameterImpl.this); + if (!(element instanceof PyReferenceExpression || element instanceof PyTargetExpression)) return false; + if (((PyQualifiedExpression)element).isQualified()) return false; + List<@Nullable PsiElement> definitions = PyUtil.multiResolveTopPriority(element, resolveContext); + return ContainerUtil.all(definitions, e -> e == PyNamedParameterImpl.this); } }); } diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py b/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py index e816b67e8945..daf7c3c598f7 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py @@ -1,4 +1,4 @@ def foo(smth, param): if smth: param = "" - print(param.smth()) \ No newline at end of file + print(param.smth()) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index c83e803469a1..9e70a7af5f92 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -4676,6 +4676,17 @@ public class Py3TypeTest extends PyTestCase { """); } + // PY-85030 + public void testStructuralTypesAttributeAccessAfterTypeNarrowingAndReassignmentInIf() { + doTest("(p: Any) -> None", """ + def f(p): + if isinstance(p, int): + p = "foo" + x = p.lower() + expr = f + """); + } + private void doTest(final String expectedType, final String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 83f33d822696..94b28be35535 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -4125,5 +4125,37 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { _: C2 = c1 """); } + + // PY-85030 + public void testStructuralTypeAndStrictUnion() { + doTestByText(""" + responses = { + 100: "abc", + } + + def process(status): + if isinstance(status, int): + status = responses[status] + return status.lower().replace(" ", "-") + + def do(arg): + title = "abc" if arg else 100 + return process(title) + """); + } + + // PY-85030 + public void testStructuralTypeAndDefiniteReassignmentUnderCondition() { + doTestByText(""" + def f(p): + if p: + p = "foo" + else: + p = "bar" + return p.lower() + + f(42) + """); + } }