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"}, ]