From 9bbd8452a07e61eb04ef1bdb8c056d67de127da9 Mon Sep 17 00:00:00 2001 From: "Aleksandr.Govenko" Date: Tue, 30 Sep 2025 07:37:59 +0000 Subject: [PATCH] PY-83047 False positive "This code is unreachable" after attribute assignment to `None` under an `if` Merge-request: IJ-MR-173356 Merged-by: Aleksandr Govenko GitOrigin-RevId: 08669d49a831548c354096fbb5cbec1a155da4e2 --- .../PyAttributeOutsideInitInspection.java | 16 ++- .../python/inspections/PyFinalInspection.kt | 19 +-- .../src/com/jetbrains/python/psi/PyUtil.java | 28 ---- .../python/psi/impl/PyClassImpl.java | 48 ++++--- .../psi/impl/PyReferenceExpressionImpl.java | 123 ++++++++++-------- .../python/refactoring/PyDefUseUtil.java | 36 ++++- .../com/jetbrains/python/Py3ResolveTest.java | 2 +- .../com/jetbrains/python/Py3TypeTest.java | 102 +++++++++++++++ .../inspections/PyFinalInspectionTest.java | 2 +- 9 files changed, 249 insertions(+), 127 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java index 4734cc1bcbd8..cafd1a1be118 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyAttributeOutsideInitInspection.java @@ -21,6 +21,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyPsiBundle; import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache; @@ -36,7 +37,10 @@ import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * User: ktisha @@ -73,12 +77,12 @@ public final class PyAttributeOutsideInitInspection extends PyInspection { } final Map localProperties = containingClass.getProperties(); - final Map declaredAttributes = new HashMap<>(); + final MultiMap declaredAttributes = new MultiMap<>(); final Set inheritedProperties = new HashSet<>(); StreamEx.of(containingClass.getClassAttributes()) .filter(attribute -> !localProperties.containsKey(attribute.getName())) - .forEach(attribute -> declaredAttributes.put(attribute.getName(), attribute)); + .forEach(attribute -> declaredAttributes.putValue(attribute.getName(), attribute)); final PyFunction initMethod = containingClass.findMethodByName(PyNames.INIT, false, myTypeEvalContext); if (initMethod != null) { @@ -91,13 +95,13 @@ public final class PyAttributeOutsideInitInspection extends PyInspection { } for (PyTargetExpression classAttr : superClass.getClassAttributes()) { - declaredAttributes.put(classAttr.getName(), classAttr); + declaredAttributes.putValue(classAttr.getName(), classAttr); } inheritedProperties.addAll(superClass.getProperties().keySet()); } - final Map attributes = new HashMap<>(); + final MultiMap attributes = new MultiMap<>(); PyClassImpl.collectInstanceAttributes(node, attributes); for (PyTargetExpression attribute : attributes.values()) { @@ -138,7 +142,7 @@ public final class PyAttributeOutsideInitInspection extends PyInspection { private static boolean isDefinedByProperty(@NotNull PyTargetExpression attribute, @NotNull Collection properties, - @NotNull Map attributesInInit) { + @NotNull MultiMap attributesInInit) { return StreamEx.of(properties) .filter(it -> isSetBy(attribute, it)) .anyMatch(it -> attributesInInit.containsKey(it.getName())); diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt index ac22e13f8179..73a886fd9634 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyFinalInspection.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.util.NlsSafe import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.containers.MultiMap import com.jetbrains.python.PyNames import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache @@ -192,10 +193,10 @@ class PyFinalInspection : PyInspection() { val classLevelFinals = mutableMapOf() cls.classAttributes.forEach { if (isFinal(it)) classLevelFinals[it.name] = it } - val initAttributes = mutableMapOf() + val initAttributes = MultiMap() cls.findMethodByName(PyNames.INIT, false, myTypeEvalContext)?.let { PyClassImpl.collectInstanceAttributes(it, initAttributes) } - return Pair(classLevelFinals, initAttributes) + return Pair(classLevelFinals, initAttributes.toHashMap().mapValues { it.value.first() }) } private fun getDeclaredClassAndInstanceFinals(cls: PyClass): Pair, Map> { @@ -212,10 +213,10 @@ class PyFinalInspection : PyInspection() { } cls.findMethodByName(PyNames.INIT, false, myTypeEvalContext)?.let { init -> - val attributesInInit = mutableMapOf() + val attributesInInit = MultiMap() PyClassImpl.collectInstanceAttributes(init, attributesInInit) - attributesInInit.keys.removeAll(instanceFinals.keys) - instanceFinals += attributesInInit.filterValues { isFinal(it) } + attributesInInit.keySet().removeAll(instanceFinals.keys) + instanceFinals += attributesInInit.toHashMap().mapValues { it.value.first() }.filterValues { isFinal(it) } } return Pair(classFinals, instanceFinals) @@ -281,9 +282,9 @@ class PyFinalInspection : PyInspection() { private fun checkInstanceFinalsOutsideInit(method: PyFunction) { if (PyUtil.isInitMethod(method)) return - val instanceAttributes = mutableMapOf() + val instanceAttributes = MultiMap() PyClassImpl.collectInstanceAttributes(method, instanceAttributes) - instanceAttributes.values.forEach { + instanceAttributes.values().forEach { if (isFinal(it)) registerProblem(it, PyPsiBundle.message("INSP.final.final.attribute.should.be.declared.in.class.body.or.init")) } } @@ -373,9 +374,9 @@ class PyFinalInspection : PyInspection() { for (current in (sequenceOf(cls) + cls.getAncestorClasses(myTypeEvalContext).asSequence())) { val init = current.findMethodByName(PyNames.INIT, false, myTypeEvalContext) if (init != null) { - val attributesInInit = mutableMapOf() + val attributesInInit = MultiMap() PyClassImpl.collectInstanceAttributes(init, attributesInInit) - if (attributesInInit[name]?.let { it != target && isFinal(it) } == true) { + if (attributesInInit[name].any { it != target && isFinal(it) }) { @NlsSafe val qualifiedName = (if (cls == current) "" else "${current.name}.") + name registerProblem(target, PyPsiBundle.message("INSP.final.final.target.could.not.be.reassigned", qualifiedName)) break diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java index fe0aa9ac0f1d..e6e447bfdc77 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyUtil.java @@ -182,34 +182,6 @@ public final class PyUtil { // TODO: move to a more proper place? - /** - * Determine the type of a special attribute. Currently supported: {@code __class__} and {@code __dict__}. - * - * @param ref reference to a possible attribute; only qualified references make sense. - * @return type, or null (if type cannot be determined, reference is not to a known attribute, etc.) - */ - public static @Nullable PyType getSpecialAttributeType(@Nullable PyReferenceExpression ref, TypeEvalContext context) { - if (ref != null) { - PyExpression qualifier = ref.getQualifier(); - if (qualifier != null) { - String attr_name = ref.getReferencedName(); - if (PyNames.__CLASS__.equals(attr_name)) { - PyType qualifierType = context.getType(qualifier); - if (qualifierType instanceof PyClassType) { - return new PyClassTypeImpl(((PyClassType)qualifierType).getPyClass(), true); // always as class, never instance - } - } - else if (PyNames.DUNDER_DICT.equals(attr_name)) { - PyType qualifierType = context.getType(qualifier); - if (qualifierType instanceof PyClassType && ((PyClassType)qualifierType).isDefinition()) { - return PyBuiltinCache.getInstance(ref).getDictType(); - } - } - } - } - return null; - } - /** * Makes sure that 'thing' is not null; else throws an {@link IncorrectOperationException}. * diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java index 2cccf641d130..bf7a7562906f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -1148,6 +1148,7 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla Map scopesToSkip = StreamEx.of(getInstanceAttributes()) .filter(e -> e.getName() != null) .mapToEntry(e -> e.getName(), e -> ScopeUtil.getScopeOwner(e)) + .distinct() .toMap(); attributes = collectInstanceAttributes(scopesToSkip); myFallbackInstanceAttributes = attributes; @@ -1155,7 +1156,7 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla } private @NotNull List collectInstanceAttributes(@NotNull Map scopesToSkip) { - Map result = new HashMap<>(); + MultiMap result = new MultiMap<>(); collectAttributesInConstructors(result, scopesToSkip); final PyFunction[] methods = getMethods(); for (PyFunction method : methods) { @@ -1164,14 +1165,14 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla return new ArrayList<>(result.values()); } - private void collectAttributesInConstructors(@NotNull Map result, + private void collectAttributesInConstructors(@NotNull MultiMap result, @NotNull Map scopesToSkip) { PyFunction newMethod = findMethodByName(PyNames.NEW, false, null); if (newMethod != null) { for (PyTargetExpression target : getTargetExpressions(newMethod)) { String name = target.getName(); if (scopesToSkip.get(name) != newMethod) { - result.put(name, target); + result.putValue(name, target); } } } @@ -1181,12 +1182,24 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla } } - public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull Map result) { + public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull MultiMap result) { collectInstanceAttributes(method, result, emptySet(), Collections.emptyMap()); } + /** + * @deprecated Use collectInstanceAttributes(com.jetbrains.python.psi.PyFunction, com.intellij.util.containers.MultiMap) + */ + @Deprecated + public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull Map result) { + MultiMap localResult = new MultiMap<>(); + collectInstanceAttributes(method, localResult, emptySet(), Collections.emptyMap()); + for (Map.Entry> entry : localResult.entrySet()) { + result.put(entry.getKey(), ContainerUtil.getFirstItem(entry.getValue())); + } + } + private static void collectInstanceAttributes(@NotNull PyFunction method, - final @NotNull Map result, + final @NotNull MultiMap result, @NotNull Set namesToSkip, @NotNull Map scopesToSkip) { final PyParameter[] params = method.getParameterList().getParameters(); @@ -1198,7 +1211,7 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla if (!namesToSkip.contains(name) && scopesToSkip.get(name) != method && PyUtil.isInstanceAttribute(target)) { - result.put(name, target); + result.putValue(name, target); } } } @@ -1354,27 +1367,10 @@ public class PyClassImpl extends PyBaseElementImpl implements PyCla @Override public boolean processInstanceLevelDeclarations(@NotNull PsiScopeProcessor processor, @Nullable PsiElement location) { - final PyFunction instanceMethod = PsiTreeUtil.getStubOrPsiParentOfType(location, PyFunction.class); - final PyClass containingClass = instanceMethod != null ? instanceMethod.getContainingClass() : null; - if (instanceMethod != null && containingClass != null && CompletionUtilCoreImpl.getOriginalElement(containingClass) == this) { - for (PyTargetExpression target : getTargetExpressions(instanceMethod)) { - if (PyUtil.isInstanceAttribute(target) && !processor.execute(target, ResolveState.initial())) { - return false; - } - } + for (PyTargetExpression expr : getInstanceAttributes()) { + if (!processor.execute(expr, ResolveState.initial())) return false; } - if (!processInstanceAttributesNotInMethod(processor, instanceMethod, getInstanceAttributes())) return false; - if (!processInstanceAttributesNotInMethod(processor, instanceMethod, getFallbackInstanceAttributes())) return false; - return true; - } - - private static boolean processInstanceAttributesNotInMethod(@NotNull PsiScopeProcessor processor, - @Nullable PyFunction instanceMethod, - @NotNull List instanceAttributes) { - for (PyTargetExpression expr : instanceAttributes) { - if (instanceMethod != null && ScopeUtil.getScopeOwner(expr) == instanceMethod) { - continue; - } + for (PyTargetExpression expr : getFallbackInstanceAttributes()) { if (!processor.execute(expr, ResolveState.initial())) return false; } return true; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java index 3cbd440c551a..d95d60a47d46 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java @@ -43,8 +43,6 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere private static final Logger LOG = Logger.getInstance(PyReferenceExpressionImpl.class); - private static final int MAX_CFG_ITERATIONS = 30; - private volatile @Nullable QualifiedName myQualifiedName = null; public PyReferenceExpressionImpl(@NotNull ASTNode astNode) { @@ -203,7 +201,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere return descriptorType.get(); } - final PyType callableType = getCallableType(context, key); + final PyType callableType = getCallableType(context); if (callableType != null) { return callableType; } @@ -215,7 +213,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere return typeFromTargets; } - private @Nullable PyType getCallableType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) { + private @Nullable PyType getCallableType(@NotNull TypeEvalContext context) { PyCallExpression callExpression = PyCallExpressionNavigator.getPyCallExpressionByCallee(this); if (callExpression != null) { return getCalleeType(callExpression, PyResolveContext.defaultContext(context)); @@ -228,19 +226,45 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere return null; } - final PyType maybe_type = PyUtil.getSpecialAttributeType(this, context); - if (maybe_type != null) return Ref.create(maybe_type); + final PyExpression qualifier = getQualifier(); + if (qualifier == null) return null; + + final String attrName = getName(); + if (attrName == null) return null; - final Ref typeOfProperty = getTypeOfProperty(context); + final PyType qualifierType = context.getType(qualifier); + + final PyType dunderClassType = getDunderClassType(qualifierType, attrName); + if (dunderClassType != null) return Ref.create(dunderClassType); + + final Ref typeOfProperty = getTypeOfProperty(qualifierType, attrName, context); if (typeOfProperty != null) { return typeOfProperty; } + // This code performs a backwards traversal through the Control Flow Graph to analyze assignments. + // It searches for WRITE instructions involving `qualifier.this_name` with the following behavior: + // + // 1. If WRITE instructions are found on all possible execution paths: + // - Returns a union type combining the types from all getType() calls on those instructions + // + // 2. If a WRITE instruction involving just the `qualifier` is found on any path + // (via PyTargetExpression or PyNamedParameter): + // - The analysis stops and returns null, ignoring any other paths + // + // (see PyDefUseUtil.getLatestDefs) + // + // Note on getType() behavior for PyTargetExpression: + // - First queries PyTypeProviders (including PyTypingTypeProvider) + // - PyTypingTypeProvider checks if qualifier's class has a type annotation for 'this_name' + // and returns that annotated type if found + // - If no providers return a type, falls back to returning the type of the assigned value + final PyType typeByControlFlow = getQualifiedReferenceTypeByControlFlow(context); if (typeByControlFlow != null) { return Ref.create(typeByControlFlow); } - + return null; } @@ -298,16 +322,6 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere return null; } - private @Nullable Ref getTypeOfProperty(@NotNull TypeEvalContext context) { - final PyExpression qualifier = getQualifier(); - final String name = getName(); - if (name != null && qualifier != null) { - final PyType qualifierType = context.getType(qualifier); - return getTypeOfProperty(qualifierType, name, context); - } - return null; - } - private @Nullable Ref getTypeOfProperty(@Nullable PyType qualifierType, @NotNull String name, @NotNull TypeEvalContext context) { if (qualifierType instanceof PyClassType classType) { final PyClass pyClass = classType.getPyClass(); @@ -346,6 +360,14 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere return null; } + private static @Nullable PyType getDunderClassType(@Nullable PyType qualifierType, @NotNull String attrName) { + if (qualifierType instanceof PyClassType classType && PyNames.__CLASS__.equals(attrName)) { + // PyInstantiableType#toClass() does not work here, as we also need to remove generic parameters + return new PyClassTypeImpl(classType.getPyClass(), true); + } + return null; + } + private @Nullable PyType getTypeFromProviders(@NotNull TypeEvalContext context) { for (PyTypeProvider provider : PyTypeProvider.EP_NAME.getExtensionList()) { try { @@ -484,45 +506,40 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere @NotNull ScopeOwner scopeOwner) { final PyAugAssignmentStatement augAssignment = PsiTreeUtil.getParentOfType(anchor, PyAugAssignmentStatement.class); final PyElement element = augAssignment != null ? augAssignment : anchor; - try { - final List defs = PyDefUseUtil.getLatestDefs(scopeOwner, name, element, true, false, context); - // null means empty set of possible types, Ref(null) means Any - final @Nullable Ref combinedType = StreamEx.of(defs) - .map(instr -> { - if (instr.getElement() == anchor) { - // exclude recursive definition (example: type of 'i++' inside a loop) - return null; - } - if (instr instanceof ReadWriteInstruction readWriteInstruction) { - return readWriteInstruction.getType(context, anchor); - } - if (instr instanceof ConditionalInstruction conditionalInstruction) { - if (context.getType((PyTypedElement)conditionalInstruction.getCondition()) instanceof PyNarrowedType narrowedType - && narrowedType.isBound()) { - var arguments = narrowedType.getOriginal().getArguments(null); - if (!arguments.isEmpty()) { - var firstArgument = arguments.get(0); - PyType type = narrowedType.getNarrowedType(); - if (firstArgument instanceof PyReferenceExpression && type != null) { - @Nullable PyType initial = context.getType(firstArgument); - boolean positive = conditionalInstruction.getResult() ^ narrowedType.getNegated(); - if (narrowedType.getTypeIs()) { - return PyTypeAssertionEvaluator.createAssertionType(initial, type, positive, false, context); - } - return Ref.create((positive) ? type : initial); + final List defs = PyDefUseUtil.getLatestDefs(scopeOwner, name, element, true, false, context); + // null means empty set of possible types, Ref(null) means Any + final @Nullable Ref combinedType = StreamEx.of(defs) + .map(instr -> { + if (instr.getElement() == anchor) { + // exclude recursive definition (example: type of 'i++' inside a loop) + return null; + } + if (instr instanceof ReadWriteInstruction readWriteInstruction) { + return readWriteInstruction.getType(context, anchor); + } + if (instr instanceof ConditionalInstruction conditionalInstruction) { + if (context.getType((PyTypedElement)conditionalInstruction.getCondition()) instanceof PyNarrowedType narrowedType + && narrowedType.isBound()) { + var arguments = narrowedType.getOriginal().getArguments(null); + if (!arguments.isEmpty()) { + var firstArgument = arguments.get(0); + PyType type = narrowedType.getNarrowedType(); + if (firstArgument instanceof PyReferenceExpression && type != null) { + @Nullable PyType initial = context.getType(firstArgument); + boolean positive = conditionalInstruction.getResult() ^ narrowedType.getNegated(); + if (narrowedType.getTypeIs()) { + return PyTypeAssertionEvaluator.createAssertionType(initial, type, positive, false, context); } + return Ref.create((positive) ? type : initial); } } } - return null; - }) - .nonNull() - .collect(PyTypeUtil.toUnionFromRef()); - return Ref.deref(combinedType); - } - catch (PyDefUseUtil.InstructionNotFoundException ignored) { - } - return null; + } + return null; + }) + .nonNull() + .collect(PyTypeUtil.toUnionFromRef()); + return Ref.deref(combinedType); } public static @Nullable Ref getReferenceTypeFromProviders(@NotNull PsiElement target, diff --git a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java index 8221cf6881f3..542f2930fd07 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/refactoring/PyDefUseUtil.java @@ -24,6 +24,7 @@ import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.Version; import com.intellij.psi.PsiElement; +import com.intellij.psi.util.QualifiedName; import com.jetbrains.python.PyLanguageFacadeKt; import com.jetbrains.python.codeInsight.controlflow.*; import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; @@ -69,10 +70,13 @@ public final class PyDefUseUtil { if (startNum < 0) { return Collections.emptyList(); } + + QualifiedName varQname = QualifiedName.fromDottedString(varName); LanguageLevel languageLevel = PyLanguageFacadeKt.getEffectiveLanguageLevel(anchor.getContainingFile()); final Collection result = new LinkedHashSet<>(); final HashMap pendingTypeGuard = new HashMap<>(); + final Ref<@NotNull Boolean> foundPrefixWrite = Ref.create(false); iteratePrev(startNum, controlFlow, instruction -> { if (instruction instanceof PyWithContextExitInstruction withExit) { @@ -101,8 +105,16 @@ public final class PyDefUseUtil { ? TypeEvalContext.codeAnalysis(context.getOrigin().getProject(), context.getOrigin()) : TypeEvalContext.codeInsightFallback(context.getOrigin().getProject()); if (newContext.getType(typedElement) instanceof PyNarrowedType narrowedType && narrowedType.isBound()) { - if (varName.equals(narrowedType.getQname())) { - pendingTypeGuard.put(narrowedType.getOriginal(), conditionalInstruction); + String narrowedQname = narrowedType.getQname(); + if (narrowedQname != null) { + if (isQualifiedBy(varQname, narrowedQname)) { + foundPrefixWrite.set(true); + return ControlFlowUtil.Operation.BREAK; + } + + if (narrowedQname.equals(varName)) { + pendingTypeGuard.put(narrowedType.getOriginal(), conditionalInstruction); + } } } } @@ -111,7 +123,17 @@ public final class PyDefUseUtil { final ReadWriteInstruction.ACCESS access = rwInstruction.getAccess(); if (access.isWriteAccess() || acceptTypeAssertions && access.isAssertTypeAccess() && instruction.num() < startNum) { - if (Comparing.strEqual(rwInstruction.getName(), varName)) { + + final String name = rwInstruction.getName(); + + if (name != null && isQualifiedBy(varQname, name)) { + if (isReachableWithVersionChecks(rwInstruction, languageLevel)){ + foundPrefixWrite.set(true); + return ControlFlowUtil.Operation.BREAK; + } + } + + if (Comparing.strEqual(name, varName)) { if (isReachableWithVersionChecks(rwInstruction, languageLevel)) { result.add(rwInstruction); } @@ -129,9 +151,17 @@ public final class PyDefUseUtil { } return ControlFlowUtil.Operation.NEXT; }); + if (foundPrefixWrite.get()) { + return Collections.emptyList(); + } return new ArrayList<>(result); } + private static boolean isQualifiedBy(QualifiedName varQname, @NotNull String qualifier) { + QualifiedName elementQname = QualifiedName.fromDottedString(qualifier); + return varQname.getComponentCount() > elementQname.getComponentCount() && varQname.matchesPrefix(elementQname); + } + private static int findStartInstructionId(@NotNull PsiElement startAnchor, @NotNull PyControlFlow flow) { PsiElement realCfgAnchor = startAnchor; final PyAugAssignmentStatement augAssignment = PyAugAssignmentStatementNavigator.getStatementByTarget(startAnchor); diff --git a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java index 09d2ad6b08d3..480de0150c29 100644 --- a/python/testSrc/com/jetbrains/python/Py3ResolveTest.java +++ b/python/testSrc/com/jetbrains/python/Py3ResolveTest.java @@ -805,7 +805,7 @@ public class Py3ResolveTest extends PyResolveTestCase { public void testInstanceAttrOtherMethodAndAbove() { final PyTargetExpression target = assertResolvesTo(PyTargetExpression.class, "foo"); final PyFunction function = assertInstanceOf(ScopeUtil.getScopeOwner(target), PyFunction.class); - assertEquals("f", function.getName()); + assertEquals("g", function.getName()); } public void testInstanceAttrBelowAndOtherMethodAbove() { diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index 1ff40a63a834..45b6fcd7aa83 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -17,6 +17,108 @@ import java.util.Map; public class Py3TypeTest extends PyTestCase { public static final String TEST_DIRECTORY = "/types/"; + + // See PyReferenceExpressionImpl.getQualifiedReferenceType for explanations. + public void testQualifiedNameResolution() { + doTest("str", """ + class C: + def m(self): + self.t = 5 + + def f(self: C, x: float): + self.t = "foo" + expr = self.t + """); + + doTest("int", """ + class C: + def m(self): + self.t: int = 5 + + def f(self: C, x: float): + self.t = "foo" + expr = self.t + """); + + doTest("int", """ + class C: + def __init__(self): + self.t: int = 5 + + def f(self: C, x: float): + self.t = "foo" + expr = self.t + """); + } + + // PY-83047 + public void testQualifiedReferenceTypeNarrowing() { + doTest("int | None", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self, x: float): + if x < 0: + self.t = None + + expr = self.t + """); + + doTest("int", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self, x: float): + if self.t is not None: + expr = self.t + """); + + doTest("None", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self, x: float): + if self.t is None: + expr = self.t + """); + + // Same, but as a separate function + + doTest("int | None", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self: C, x: float): + if x < 0: + self.t = None + + expr = self.t + """); + + doTest("int", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self: C, x: float): + if self.t is not None: + expr = self.t + """); + + doTest("None", """ + class C: + def __init__(self): + self.t: int | None = 5 + + def f(self: C, x: float): + if self.t is None: + expr = self.t + """); + } /** Overload signatures for dict.get and dict.pop in builtins.pyi differ slightly, diff --git a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java index fa82131a67a1..b8152ffbed3d 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyFinalInspectionTest.java @@ -353,7 +353,7 @@ public class PyFinalInspectionTest extends PyInspectionTestCase { self.a: Final[str] = "str" def method(self): - self.a: Final[int] = 10 + self.a: Final[int] = 10 self.b: Final[int] = 10""") ); }