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 <aleksandr.govenko@jetbrains.com>

GitOrigin-RevId: 08669d49a831548c354096fbb5cbec1a155da4e2
This commit is contained in:
Aleksandr.Govenko
2025-09-30 07:37:59 +00:00
committed by intellij-monorepo-bot
parent 7d7c958cde
commit 9bbd8452a0
9 changed files with 249 additions and 127 deletions
@@ -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<String, Property> localProperties = containingClass.getProperties();
final Map<String, PyTargetExpression> declaredAttributes = new HashMap<>();
final MultiMap<String, PyTargetExpression > declaredAttributes = new MultiMap<>();
final Set<String> 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<String, PyTargetExpression> attributes = new HashMap<>();
final MultiMap<String, PyTargetExpression> 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<Property> properties,
@NotNull Map<String, PyTargetExpression> attributesInInit) {
@NotNull MultiMap<String, PyTargetExpression> attributesInInit) {
return StreamEx.of(properties)
.filter(it -> isSetBy(attribute, it))
.anyMatch(it -> attributesInInit.containsKey(it.getName()));
@@ -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<String?, PyTargetExpression>()
cls.classAttributes.forEach { if (isFinal(it)) classLevelFinals[it.name] = it }
val initAttributes = mutableMapOf<String, PyTargetExpression>()
val initAttributes = MultiMap<String, PyTargetExpression>()
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<String, PyTargetExpression>, Map<String, PyTargetExpression>> {
@@ -212,10 +213,10 @@ class PyFinalInspection : PyInspection() {
}
cls.findMethodByName(PyNames.INIT, false, myTypeEvalContext)?.let { init ->
val attributesInInit = mutableMapOf<String, PyTargetExpression>()
val attributesInInit = MultiMap<String, PyTargetExpression>()
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<String, PyTargetExpression>()
val instanceAttributes = MultiMap<String, PyTargetExpression>()
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<String, PyTargetExpression>()
val attributesInInit = MultiMap<String, PyTargetExpression>()
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
@@ -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}.
*
@@ -1148,6 +1148,7 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
Map<String, ScopeOwner> 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<PyClassStub> implements PyCla
}
private @NotNull List<PyTargetExpression> collectInstanceAttributes(@NotNull Map<String, ScopeOwner> scopesToSkip) {
Map<String, PyTargetExpression> result = new HashMap<>();
MultiMap<String, PyTargetExpression> result = new MultiMap<>();
collectAttributesInConstructors(result, scopesToSkip);
final PyFunction[] methods = getMethods();
for (PyFunction method : methods) {
@@ -1164,14 +1165,14 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
return new ArrayList<>(result.values());
}
private void collectAttributesInConstructors(@NotNull Map<String, PyTargetExpression> result,
private void collectAttributesInConstructors(@NotNull MultiMap<String, PyTargetExpression> result,
@NotNull Map<String, ScopeOwner> 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<PyClassStub> implements PyCla
}
}
public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull Map<String, PyTargetExpression> result) {
public static void collectInstanceAttributes(@NotNull PyFunction method, final @NotNull MultiMap<String, PyTargetExpression> 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<String, PyTargetExpression> result) {
MultiMap<String, PyTargetExpression> localResult = new MultiMap<>();
collectInstanceAttributes(method, localResult, emptySet(), Collections.emptyMap());
for (Map.Entry<String, Collection<PyTargetExpression>> entry : localResult.entrySet()) {
result.put(entry.getKey(), ContainerUtil.getFirstItem(entry.getValue()));
}
}
private static void collectInstanceAttributes(@NotNull PyFunction method,
final @NotNull Map<String, PyTargetExpression> result,
final @NotNull MultiMap<String, PyTargetExpression> result,
@NotNull Set<String> namesToSkip,
@NotNull Map<String, ScopeOwner> scopesToSkip) {
final PyParameter[] params = method.getParameterList().getParameters();
@@ -1198,7 +1211,7 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> 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<PyClassStub> 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<PyTargetExpression> 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;
@@ -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<PyType> typeOfProperty = getTypeOfProperty(context);
final PyType qualifierType = context.getType(qualifier);
final PyType dunderClassType = getDunderClassType(qualifierType, attrName);
if (dunderClassType != null) return Ref.create(dunderClassType);
final Ref<PyType> 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<PyType> 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<PyType> 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<Instruction> defs = PyDefUseUtil.getLatestDefs(scopeOwner, name, element, true, false, context);
// null means empty set of possible types, Ref(null) means Any
final @Nullable Ref<PyType> 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<Instruction> defs = PyDefUseUtil.getLatestDefs(scopeOwner, name, element, true, false, context);
// null means empty set of possible types, Ref(null) means Any
final @Nullable Ref<PyType> 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<PyType> getReferenceTypeFromProviders(@NotNull PsiElement target,
@@ -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<Instruction> result = new LinkedHashSet<>();
final HashMap<PyCallSiteExpression, ConditionalInstruction> 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);
@@ -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() {
@@ -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,
@@ -353,7 +353,7 @@ public class PyFinalInspectionTest extends PyInspectionTestCase {
self.a: Final[str] = "str"
def method(self):
<warning descr="'Final' attribute should be declared in class body or '__init__'">self.a</warning>: Final[int] = 10
<warning descr="'Final' attribute should be declared in class body or '__init__'"><warning descr="Already declared name could not be redefined as 'Final'">self.a</warning></warning>: Final[int] = 10
<warning descr="'Final' attribute should be declared in class body or '__init__'">self.b</warning>: Final[int] = 10""")
);
}