From 3e7b16f248a5819886d28309648bf44a914b9014 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Fri, 30 May 2025 16:38:28 +0300 Subject: [PATCH] PY-24834 Implement the strict semantics of union types Under the registry flag "python.typing.strict.unions", enabled by default. "Strict" union is considered a subtype of another type only if *all* its members are subtypes of this type. Similarly, accessing an attribute on a union type considered safe only if *all* its members contain this attribute. It holds even if one of the union members is Any, i.e. "str | Any" is still incompatible with "int", and it's not safe to access a non-str attribute on it. This is how all other type checkers treat union types. Historically, we use so called "weak unions", where the above rules were relaxed so that it was enough to have at least one compatible member type. It was necessary because back then there was no way to declare function overloads, type narrowing was limited (no pattern matching, no TypeIs/TypeGuard), so proper strict unions, as in the type theory, would cause too many false positives. Co-authored-by: Aleksandr.Govenko GitOrigin-RevId: 8a676d810548bf71ac0ff3497e59d8694e03e4db --- .../intellij.python.community.impl.xml | 2 + .../resources/messages/PyPsiBundle.properties | 1 + .../python/documentation/PyTypeRenderer.java | 2 +- .../PyUnresolvedReferencesVisitor.java | 53 ++++++++++- .../jetbrains/python/psi/types/PyABCUtil.java | 6 +- .../python/psi/types/PyTypeChecker.java | 94 +++++++++++-------- .../python/psi/types/PyTypedDictType.kt | 6 +- .../ForLoopIteration.py | 2 +- .../IterateOverDictValueWhenItsTypeIsUnion.py | 2 +- .../ListLiteralAgainstTypingLiteral.py | 3 +- .../MapReturnElementType.py | 2 +- .../PyTypeCheckerInspection/NotNone.py | 4 +- .../SetLiteralAgainstTypingLiteral.py | 3 +- .../StrictUnionImplicitProtocolMatching.py | 51 ++++++++++ .../PyTypeCheckerInspection/TypeAssertions.py | 4 +- .../UnionReturnTypes.py | 2 +- .../AsyncAwaitWarningOnImportedFun/a.py | 2 +- .../a.py | 2 +- .../CustomNewReturnInAnotherModule/a.py | 2 +- .../listIndexedByUnknownType.py | 2 +- .../strictUnionMemberAttributeAccess.py | 31 ++++++ .../strictUnionMemberOperatorAccess.py | 63 +++++++++++++ .../Py3TypeCheckerInspectionTest.java | 7 +- ...Py3UnresolvedReferencesInspectionTest.java | 10 ++ .../PyTypeCheckerInspectionTest.java | 4 +- 25 files changed, 296 insertions(+), 64 deletions(-) create mode 100644 python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 91691a259553..c7ba406e7bca 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -669,6 +669,8 @@ + + diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 93c08f396718..8f2621d46a31 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -936,6 +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}'' 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/documentation/PyTypeRenderer.java b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java index 6efa1e553d87..fdf9d8d6cc76 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java +++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java @@ -297,7 +297,7 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk if (ContainerUtil.all(unionType.getMembers(), t -> t instanceof PyClassType ct && ct.isDefinition())) { return wrapInTypingType(render(unionType.map(type -> type != null ? ((PyClassType)type).toInstance() : null))); } - if (PyTypeChecker.isUnknown(unionType, false, myTypeEvalContext)) { + if (unionType.isWeak()) { // Always put Any at the end of the union return renderUnion(List.of(render(unionType.excludeNull()), visitUnknownType())); } 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 f4c5e3960e3a..f3befebd9c19 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,6 +12,7 @@ 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; @@ -26,6 +27,7 @@ import com.jetbrains.python.codeInsight.PyCustomMember; import com.jetbrains.python.codeInsight.PySubstitutionChunkReference; import com.jetbrains.python.codeInsight.controlflow.PyDataFlowKt; import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider; +import com.jetbrains.python.documentation.PythonDocumentationProvider; import com.jetbrains.python.documentation.docstrings.DocStringParameterReference; import com.jetbrains.python.documentation.docstrings.DocStringTypeReference; import com.jetbrains.python.inspections.PyInspectionExtension; @@ -152,6 +154,29 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor !isContainingFileImportAllowed(node, (PsiFile)target)) { registerProblem(node, PyPsiBundle.message("INSP.unresolved.refs.import.resolves.to.its.containing.file")); } + else if (Registry.is("python.typing.strict.unions", true) && node instanceof PyQualifiedExpression qualifiedExpression) { + String referencedName = qualifiedExpression.getReferencedName(); + PyExpression qualifier = qualifiedExpression.getQualifier(); + 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); + registerProblem( + node, + PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", qualifierTypeRender, referencedName), + ProblemHighlightType.WEAK_WARNING, + null, + reference.getRangeInElement() + ); + } + } + } + } } private boolean isAwaitCallToImportedNonAsyncFunction(@NotNull PsiReference reference) { @@ -285,6 +310,11 @@ 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()); } @@ -310,6 +340,9 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor if (severity == HighlightSeverity.WARNING) { hlType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING; } + if (severity == HighlightSeverity.WEAK_WARNING) { + hlType = ProblemHighlightType.WEAK_WARNING; + } else if (severity == HighlightSeverity.ERROR) { hlType = ProblemHighlightType.GENERIC_ERROR; } @@ -354,6 +387,12 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor 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; + } + return ContainerUtil.exists(unionType.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; @@ -403,9 +442,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor return true; } } - if (type instanceof PyUnionType) { - return ContainerUtil.exists(((PyUnionType)type).getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); - } if (type instanceof PyModuleType) { final PyFile module = ((PyModuleType)type).getModule(); if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37)) { @@ -420,6 +456,17 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor return false; } + private boolean unresolvedInUnionTypeContainingUnknownMembers(@NotNull PyType type, @NotNull String name) { + if (!(type instanceof PyUnionType unionType)) { + return false; + } + boolean unresolvedInSomeUnionMembers = ContainerUtil.exists(unionType.getMembers(), t -> { + return !PyTypeChecker.isUnknown(t, false, myTypeEvalContext) && + ContainerUtil.isEmpty(t.resolveMember(name, null, AccessDirection.READ, getResolveContext())); + }); + return unresolvedInSomeUnionMembers && unionType.isWeak(); + } + private boolean isDecoratedAsDynamic(@NotNull PyClass cls, boolean inherited) { if (inherited) { if (isDecoratedAsDynamic(cls, false)) { diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java index d19b04f79bf8..4093b2d279ee 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.psi.types; +import com.intellij.openapi.util.registry.Registry; import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.PyClass; import org.jetbrains.annotations.NotNull; @@ -124,7 +125,10 @@ public final class PyABCUtil { } } if (type instanceof PyUnionType) { - return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context)); + if (!Registry.is("python.typing.strict.unions", true)) { + return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context)); + } + return PyTypeUtil.toStream(type).nonNull().allMatch(it -> isSubtype(it, superClassName, context)); } return false; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java index 140212f1f415..f8dc63ebc375 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -2,6 +2,7 @@ package com.jetbrains.python.psi.types; import com.intellij.openapi.util.*; +import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.util.ArrayUtil; @@ -466,12 +467,13 @@ public final class PyTypeChecker { } } - // checking strictly separately until PY-24834 gets implemented - if (ContainerUtil.exists(actual.getMembers(), x -> x instanceof PyLiteralStringType || x instanceof PyLiteralType)) { - return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false)); + if (!Registry.is("python.typing.strict.unions", true)) {// checking strictly separately until PY-24834 gets implemented + if (ContainerUtil.exists(actual.getMembers(), x -> x instanceof PyLiteralStringType || x instanceof PyLiteralType)) { + return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false)); + } + return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false)); } - - return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false)); + return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false)); } private static @NotNull Optional match(@NotNull PyTupleType expected, @NotNull PyUnionType actual, @NotNull MatchContext context) { @@ -1039,11 +1041,10 @@ public final class PyTypeChecker { } } if (type instanceof PyUnionType union) { - for (PyType t : union.getMembers()) { - if (isUnknown(t, genericsAreUnknown, context)) { - return true; - } + if (!Registry.is("python.typing.strict.unions", true)) { + return ContainerUtil.exists(union.getMembers(), member -> isUnknown(member, genericsAreUnknown, context)); } + return ContainerUtil.all(union.getMembers(), member -> isUnknown(member, genericsAreUnknown, context)); } return false; } @@ -1418,37 +1419,52 @@ public final class PyTypeChecker { @Nullable PyType actualType, @NotNull GenericSubstitutions substitutions, @NotNull TypeEvalContext context) { - // TODO find out a better way to pass the corresponding function inside - final PyParameter param = paramWrapper.getParameter(); - final PyFunction function = as(ScopeUtil.getScopeOwner(param), PyFunction.class); - assert function != null; - if (function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD) { - actualType = PyTypeUtil.toStream(actualType) - .select(PyClassLikeType.class) - .map(PyClassLikeType::toClass) - .select(PyType.class) - .foldLeft(PyUnionType::union) - .orElse(actualType); - } - else if (PyUtil.isInitMethod(function)) { - actualType = PyTypeUtil.toStream(actualType) - .select(PyInstantiableType.class) - .map(PyInstantiableType::toInstance) - .select(PyType.class) - .foldLeft(PyUnionType::union) - .orElse(actualType); - } + // TODO find out a better way to pass the corresponding function inside + final PyParameter param = paramWrapper.getParameter(); + final PyFunction function = as(ScopeUtil.getScopeOwner(param), PyFunction.class); + assert function != null; + if (function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD) { + actualType = PyTypeUtil.toStream(actualType) + .select(PyClassLikeType.class) + .map(PyClassLikeType::toClass) + .select(PyType.class) + .foldLeft(PyUnionType::union) + .orElse(actualType); + } + else if (PyUtil.isInitMethod(function)) { + actualType = PyTypeUtil.toStream(actualType) + .select(PyInstantiableType.class) + .map(PyInstantiableType::toInstance) + .select(PyType.class) + .foldLeft(PyUnionType::union) + .orElse(actualType); + } + if (Registry.is("python.typing.strict.unions", true)) { + PyClass pyClass = function.getContainingClass(); + assert pyClass != null; + PyClassLikeType classType = as(context.getType(pyClass), PyClassLikeType.class); + assert classType != null; + PyClassLikeType superType = + function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD || PyUtil.isNewMethod(function) ? classType : classType.toInstance(); + // In a union receiver type, leave only members that actually have this function + // TODO how does it work with qualified calls, e.g. SomeClass.method(receiver, arg1, arg2) + // TODO how does it work with @classmethods? + actualType = PyTypeUtil.toStream(actualType) + .filter(type -> match(superType, type, context)) + .collect(PyTypeUtil.toUnion()); + } - PyClass containingClass = function.getContainingClass(); - assert containingClass != null; - PyType genericClass = findGenericDefinitionType(containingClass, context); - if (genericClass instanceof PyInstantiableType instantiableType && (isNewMethod(function) || function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD)) { - genericClass = instantiableType.toClass(); - } - if (genericClass != null && !match(genericClass, expectedType, context, substitutions)) { - return null; - } - return actualType; + PyClass containingClass = function.getContainingClass(); + assert containingClass != null; + PyType genericClass = findGenericDefinitionType(containingClass, context); + if (genericClass instanceof PyInstantiableType instantiableType && + (isNewMethod(function) || function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD)) { + genericClass = instantiableType.toClass(); + } + if (genericClass != null && !match(genericClass, expectedType, context, substitutions)) { + return null; + } + return actualType; } private static boolean matchParameterArgumentTypes(@NotNull PyCallableParameter paramWrapper, diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt index 39aa2cf177f3..e528b6a5e3af 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt @@ -1,6 +1,7 @@ // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.psi.types +import com.intellij.openapi.util.registry.Registry import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.python.PyNames import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider @@ -206,7 +207,10 @@ class PyTypedDictType @JvmOverloads constructor( } private fun strictUnionMatch(expected: PyType?, actual: PyType?, context: TypeEvalContext): Boolean { - return PyTypeUtil.toStream(actual).allMatch { type -> PyTypeChecker.match(expected, type, context) } + if (!Registry.`is`("python.typing.strict.unions", true)) { + return PyTypeUtil.toStream(actual).allMatch { type -> PyTypeChecker.match(expected, type, context) } + } + return PyTypeChecker.match(expected, actual, context) } /** diff --git a/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py b/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py index 807bc4926a77..2196ff6c65e3 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py +++ b/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py @@ -5,7 +5,7 @@ def test(p1): for x in 42: pass - for x in f('foo', p1): + for x in f('foo', p1): pass diff --git a/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py b/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py index 7f786045c9d1..665923f96eaf 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py +++ b/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py @@ -3,5 +3,5 @@ KWARGS = { "little_list": ['WORLD_RET_BP_IMPALA_AB.Control', 'WORLD_RET_BP_IMPALA_AB.Impala_WS'], } -for element in KWARGS["little_list"]: +for element in KWARGS["little_list"]: print(element) \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py b/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py index 29a5800a4451..2799f9a3d9cf 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py +++ b/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py @@ -15,5 +15,4 @@ list_union_literal_incorrect: List[Union[L2, L1]] = ['a', 'r'] list_tuple: List[Tuple[L2, L1]] = [('b', 'test'), (5, 'test')] -# TODO false negative due to PY-24834 -list_tuple_incorrect: List[Tuple[L2, L1]] = [('a',), (5, 'test')] \ No newline at end of file +list_tuple_incorrect: List[Tuple[L2, L1]] = [('a',), (5, 'test')] \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py index 4f180addc5c1..0c32344b04da 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py @@ -1,5 +1,5 @@ def test(): xs = map(lambda x: x + 1, [1, 2, 3]) - print('foo' + xs[0]) + print('foo' + xs[0]) ys = map(tuple, iter([[1, 2, 3]])) print(1 + ys[0], 'bar' + ys[1]) diff --git a/python/testData/inspections/PyTypeCheckerInspection/NotNone.py b/python/testData/inspections/PyTypeCheckerInspection/NotNone.py index dd9f2d8806ab..2f5942613b9c 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/NotNone.py +++ b/python/testData/inspections/PyTypeCheckerInspection/NotNone.py @@ -14,8 +14,8 @@ def test(): x1 = f1() x2 = f2() x3 = 1 - f(x1) # Weaker union types - f(x2) # Weaker union types + f(x1) + f(x2) f(x3) if x1: f(x1) diff --git a/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py b/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py index 23e2a28ea656..a903a0af30ce 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py +++ b/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py @@ -15,5 +15,4 @@ set_union_literal_incorrect: Set[Union[L2, L1]] = {'a', 'r'} set_of_tuple_and_list: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'test'), ['test', 'test'], (5, 'test')} -# TODO false negative due to PY-24834 -set_of_tuple_and_list_incorrect: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'r'), ['test', 'test'], (5, 'test')} \ No newline at end of file +set_of_tuple_and_list_incorrect: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'r'), ['test', 'test'], (5, 'test')} \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py b/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py new file mode 100644 index 000000000000..90f071cfb01c --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py @@ -0,0 +1,51 @@ +from typing import Any + + +class A: + def __iter__(self): + return self + + def __next__(self): + return 42 + + +class B: + def __iter__(self): + return self + + def __next__(self): + return 42 + + +class C: + pass + + +def all_union_members_match_no_any(iterable: A | B): + for _ in iterable: + pass + + +def some_union_members_match_no_any(iterable: A | B | None): + for _ in iterable: + pass + + +def all_union_members_dont_match_no_any(iterable: C | None): + for _ in iterable: + pass + + +def all_union_members_match_with_any(iterable: A | B | Any): + for _ in iterable: + pass + + +def some_union_members_match_with_any(iterable: A | B | None | Any): + for _ in iterable: + pass + + +def all_union_members_dont_match_with_any(iterable: C | None | Any): + for _ in iterable: + pass diff --git a/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py b/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py index 99a34191b5d4..50307998814c 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py +++ b/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py @@ -37,8 +37,8 @@ def test(): :type x: int or str """ x_1 = f_1() - print_int(x_1) # Weaker union types - print_int_or_str(x_1) # Weaker union types + print_int(x_1) + print_int_or_str(x_1) if isinstance(x_1, int): print_int(x_1) if isinstance(x_1, str): diff --git a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py index 0a9a0263507b..5e146505ae5c 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py +++ b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py @@ -16,7 +16,7 @@ def test(c): :type x: int """ x1 = f1(c) - f2(x1) # Weaker union types + f2(x1) f3(x1) f2(x1.count('')) diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py index 7135b2811861..5cf05c2b8c2e 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py @@ -6,7 +6,7 @@ async def expect_no_warning(): async def expect_new_warning(): - await fun_non_async() + await fun_non_async() def local_fun_non_async(): diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py index 403734c64dd0..fe94a40833d7 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py @@ -2,7 +2,7 @@ from b import fun_awaitable_imported, MyAwaitable async def expect_false_positive_warning(): - await fun_awaitable_imported() + await fun_awaitable_imported() async def expect_pass_1(): diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py index 5e6d4a745397..33d78e10e71f 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py @@ -1,4 +1,4 @@ from b import C c = C() -c.foo() +c.foo() diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py b/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py index d736117e174f..b534020f25e7 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/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py new file mode 100644 index 000000000000..a40b256e9844 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py @@ -0,0 +1,31 @@ +from typing import Any + + +class A: + def method(self): + pass + +class B(A): + pass + +class C: + def method(self): + pass + +def union_with_all_compatible_types(x: A | B | C): + x.method() + +def union_with_some_incompatible_types(x: A | None): + x.method() + +def union_with_all_incompatible_types(x: object | None): + x.method() + +def union_with_some_incompatible_types_and_any(x: Any | None): + x.method() + +def narrowing_union_with_some_incompatible_types_after(x: Any | None): + if isinstance(x, A): + x.method() + assert isinstance(x, B) + x.method() \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py new file mode 100644 index 000000000000..ddadd640ada4 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py @@ -0,0 +1,63 @@ +from typing import Any + + +class A: + def __pos__(self): + pass + + def __add__(self, other): + pass + + def __getitem__(self, item): + pass + + +class B: + def __pos__(self): + pass + + def __add__(self, other): + pass + + def __getitem__(self, item): + pass + + +class C: + pass + + +def all_union_members_match_no_any(x: A | B): + print(+x) + print(x + 1) + print(x[42]) + + +def some_union_members_match_no_any(x: A | B | None): + 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]) + + +def all_union_members_match_with_any(x: A | B | Any): + print(+x) + print(x + 1) + print(x[42]) + + +def some_union_members_match_with_any(x: A | B | None | Any): + 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]) diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 27a35457dd49..e437033f8ad7 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -3085,7 +3085,7 @@ def foo(param: str | int) -> TypeGuard[str]: call = empty """); } - + public void testNoWarningIfUnreachable() { doTestByText(""" def foo() -> int: @@ -3093,4 +3093,9 @@ def foo(param: str | int) -> TypeGuard[str]: return "42" # no warning here, because it is unreachable """); } + + // PY-24834 + public void testStrictUnionImplicitProtocolMatching() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index 453540b9383f..f82b97d6ace0 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -476,4 +476,14 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase """); } + + // PY-24834 + public void testStrictUnionMemberAttributeAccess() { + doTest(); + } + + // PY-24834 + public void testStrictUnionMemberOperatorAccess() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index 089af5e572c4..8cce3b44e7e3 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -1526,10 +1526,10 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { title: str year: int - movies1: list[Movie] = [ + movies1: list[Movie] = [ {"title": "Blade Runner", "year": 1982}, # OK {"title": "The Matrix"}, - ] + ] movies2: list[Movie] = [ {"title": "The Matrix"}, ]