diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 8f2621d46a31..614e53d48ca5 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -936,7 +936,7 @@ INSP.unresolved.refs.class.object.has.no.attribute=''{0}'' object has no attribu INSP.unresolved.refs.import.resolves.to.its.containing.file=Import resolves to its containing file INSP.unresolved.refs.class.does.not.define.operator=Class ''{0}'' does not define ''{1}'', so the ''{2}'' operator cannot be used on its instances INSP.unresolved.refs.ignore.references.label=Ignored references: -INSP.unresolved.refs.unresolved.attribute.in.union.type=Some members of ''{0}'' don''t have attribute ''{1}'' +INSP.unresolved.refs.unresolved.attribute.in.union.type=Member ''{0}'' of ''{1}'' does not have attribute ''{2}'' unresolved.docstring.param.reference=Function ''{0}'' does not have a parameter ''{1}'' unresolved.import.reference=No module named ''{0}'' diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java index 92f42ed8de30..e3ef583f62ea 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java @@ -12,7 +12,6 @@ import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.Version; -import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; @@ -54,6 +53,7 @@ import org.jetbrains.annotations.VisibleForTesting; import java.util.*; import static com.jetbrains.python.PyNames.END_WILDCARD; +import static com.jetbrains.python.psi.PyUtil.as; import static com.jetbrains.python.psi.impl.stubs.PyVersionSpecificStubBaseKt.evaluateVersionsForElement; public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor { @@ -160,15 +160,13 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor if (referencedName != null && qualifier != null) { PyType qualifierType = myTypeEvalContext.getType(qualifier); if (qualifierType instanceof PyUnionType unionType) { - boolean unresolvedInSomeUnionMembers = ContainerUtil.exists(unionType.getMembers(), t -> { - return !PyTypeChecker.isUnknown(t, false, myTypeEvalContext) && - ContainerUtil.isEmpty(t.resolveMember(referencedName, qualifiedExpression, AccessDirection.READ, getResolveContext())); - }); - if (unresolvedInSomeUnionMembers) { - String qualifierTypeRender = PythonDocumentationProvider.getTypeName(qualifierType, myTypeEvalContext); + PyType unionMemberMissingAttr = findStrictUnionMemberMissingAttribute(unionType, reference, referencedName); + if (unionMemberMissingAttr != null) { + String unionTypeRender = PythonDocumentationProvider.getTypeName(qualifierType, myTypeEvalContext); + String unionMemberRender = PythonDocumentationProvider.getTypeName(unionMemberMissingAttr, myTypeEvalContext); registerProblem( node, - PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", qualifierTypeRender, referencedName), + PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", unionMemberRender, unionTypeRender, referencedName), ProblemHighlightType.WEAK_WARNING, null, reference.getRangeInElement() @@ -310,13 +308,18 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor description = PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.for.class", refText, type.getName()); } } - else if (unresolvedInUnionTypeContainingUnknownMembers(type, refName)) { - String typeRender = PythonDocumentationProvider.getTypeName(type, myTypeEvalContext); - description = PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", typeRender, refName); - severity = HighlightSeverity.WEAK_WARNING; - } else { - description = PyPsiBundle.message("INSP.unresolved.refs.cannot.find.reference.in.type", refText, type.getName()); + PyType unionMemberWithoutAttr = findStrictUnionMemberMissingAttribute(type, reference, refName); + if (unionMemberWithoutAttr != null) { + String unionTypeRender = PythonDocumentationProvider.getTypeName(type, myTypeEvalContext); + String unionMemberRender = PythonDocumentationProvider.getTypeName(unionMemberWithoutAttr, myTypeEvalContext); + description = + PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", unionMemberRender, unionTypeRender, refName); + severity = HighlightSeverity.WEAK_WARNING; + } + else { + description = PyPsiBundle.message("INSP.unresolved.refs.cannot.find.reference.in.type", refText, type.getName()); + } } markedQualified = true; } @@ -383,23 +386,26 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor .anyMatch(attrName::equals); } - private boolean ignoreUnresolvedMemberForType(@NotNull PyType type, PsiReference reference, String name) { + private boolean ignoreUnresolvedMemberForType(@Nullable PyType type, @NotNull PsiReference reference, @NotNull String name) { if (type instanceof PyTypeVarType typeVarType) { return typeVarType.getBound() == null && typeVarType.getDefaultType() == null && typeVarType.getConstraints().isEmpty(); } if (type instanceof PyUnionType unionType) { - if (unresolvedInUnionTypeContainingUnknownMembers(unionType, name)) { - return false; + if (PyUnionType.isStrictSemanticsEnabled()) { + // If strict unions are enabled, we should report an error even if a union contains Any, e.g. in + // x: int | Any + // x.foo() # 'foo' access should be reported despite Any + return findStrictUnionMemberMissingAttribute(unionType, reference, name) == null; } return ContainerUtil.exists(unionType.getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); } + if (type instanceof PyUnsafeUnionType weakUnionType) { + return ContainerUtil.exists(weakUnionType.getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); + } if (PyTypeChecker.isUnknown(type, myTypeEvalContext)) { // this almost always means that we don't know the type, so don't show an error in this case return true; } - if (type instanceof PyUnsafeUnionType weakUnionType) { - return ContainerUtil.exists(weakUnionType.getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); - } if (type instanceof PyStructuralType && ((PyStructuralType)type).isInferredFromUsages()) { return true; } @@ -459,15 +465,25 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor return false; } - private boolean unresolvedInUnionTypeContainingUnknownMembers(@NotNull PyType type, @NotNull String name) { - if (!(type instanceof PyUnionType unionType)) { - return false; + private @Nullable PyType findStrictUnionMemberMissingAttribute(@NotNull PyType type, @NotNull PsiReference ref, @NotNull String name) { + if (!(type instanceof PyUnionType unionType) || !PyUnionType.isStrictSemanticsEnabled()) { + return null; } - boolean unresolvedInSomeUnionMembers = ContainerUtil.exists(unionType.getMembers(), t -> { - return !PyTypeChecker.isUnknown(t, false, myTypeEvalContext) && - ContainerUtil.isEmpty(t.resolveMember(name, null, AccessDirection.READ, getResolveContext())); + // In cases like the following (see PyUnusedImportTest#testModuleAndSubmodule): + // + // import pkg.mod + // import pkg + // pkg.mod + // ^ + // + // The type of `pkg` is a union of PyModuleType('pkg/__init__.py') and PyImportedModuleType('import pkg'). + // Only the last one owns the attribute `mod` directly, and the first needs a location to inspect imports + // of this module in the file the original reference belongs to. + PyExpression location = as(ref.getElement(), PyExpression.class); + return ContainerUtil.find(unionType.getMembers(), t -> { + if (t == null || ignoreUnresolvedMemberForType(t, ref, name)) return false; + return ContainerUtil.isEmpty(t.resolveMember(name, location, AccessDirection.READ, getResolveContext())); }); - return unresolvedInSomeUnionMembers && unionType.isWeak(); } private boolean isDecoratedAsDynamic(@NotNull PyClass cls, boolean inherited) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyStructuralType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyStructuralType.java index caf4e23790c8..0c3dba304937 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyStructuralType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyStructuralType.java @@ -27,10 +27,7 @@ import com.jetbrains.python.psi.resolve.RatedResolveResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; public class PyStructuralType implements PyType { private final @NotNull Set myAttributes; @@ -87,4 +84,16 @@ public class PyStructuralType implements PyType { public Set getAttributeNames() { return myAttributes; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PyStructuralType type = (PyStructuralType)o; + return myInferredFromUsages == type.myInferredFromUsages && Objects.equals(myAttributes, type.myAttributes); + } + + @Override + public int hashCode() { + return Objects.hash(myAttributes, myInferredFromUsages); + } } diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py b/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py index b534020f25e7..eba15acbd943 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py @@ -1,6 +1,6 @@ def f(i): xs = [] - xs[i].items() + xs[i].items() def g(index): diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py b/python/testData/inspections/PyUnresolvedReferencesInspection/unionContainingUnknownType.py index 46553e364fee..e816b67e8945 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/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py index a40b256e9844..2bd363450319 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py @@ -16,13 +16,13 @@ def union_with_all_compatible_types(x: A | B | C): x.method() def union_with_some_incompatible_types(x: A | None): - x.method() + x.method() def union_with_all_incompatible_types(x: object | None): - x.method() + x.method() def union_with_some_incompatible_types_and_any(x: Any | None): - x.method() + x.method() def narrowing_union_with_some_incompatible_types_after(x: Any | None): if isinstance(x, A): diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberExtendingAny.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberExtendingAny.py new file mode 100644 index 000000000000..4af4a6f0ae29 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberExtendingAny.py @@ -0,0 +1,13 @@ +from typing import Any + + +class A: + a = 1 + +class Anish(Any): + pass + +a = A() if bool() else Anish() + +_ = a.a +_ = a.b \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py index ddadd640ada4..da17b4072e92 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py @@ -34,15 +34,15 @@ def all_union_members_match_no_any(x: A | B): def some_union_members_match_no_any(x: A | B | None): - print(+x) - print(x + 1) - print(x[42]) + print(+x) + print(x + 1) + print(x[42]) def all_union_members_dont_match_no_any(x: C | None): - print(+x) - print(x + 1) - print(x[42]) + print(+x) + print(x + 1) + print(x[42]) def all_union_members_match_with_any(x: A | B | Any): @@ -52,12 +52,12 @@ def all_union_members_match_with_any(x: A | B | Any): def some_union_members_match_with_any(x: A | B | None | Any): - print(+x) - print(x + 1) - print(x[42]) + print(+x) + print(x + 1) + print(x[42]) def all_union_members_dont_match_with_any(x: C | None | Any): - print(+x) - print(x + 1) - print(x[42]) + print(+x) + print(x + 1) + print(x[42]) diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index f82b97d6ace0..255078ae97c0 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -486,4 +486,9 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase public void testStrictUnionMemberOperatorAccess() { doTest(); } + + // PY-24834 + public void testStrictUnionMemberExtendingAny() { + doTest(); + } }