diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java index 7adbe3701759..577317c0f35d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java @@ -316,6 +316,7 @@ public final class PyTypeHintGenerationUtil { isNoneType(type) || // Will be rendered as just Any type instanceof PyUnsafeUnionType || + type instanceof PyIntersectionType || type instanceof PyTypeParameterType) { return; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 24dee9a2b6f2..08b06558b014 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -921,6 +921,10 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< if (unionType != null) { return unionType; } + final Ref intersectionType = getIntersectionType(resolved, context); + if (intersectionType != null) { + return intersectionType; + } final PyType concatenateType = getConcatenateType(resolved, context); if (concatenateType != null) { return Ref.create(concatenateType); @@ -1071,6 +1075,22 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< return Ref.create(new PyModuleType(firstModuleInitFile)); } + private static Ref getIntersectionType(@NotNull PsiElement resolved, @NotNull PyTypingTypeProvider.Context context) { + if (resolved instanceof PyBinaryExpression expression && expression.getOperator() == PyTokenTypes.AND) { + PyExpression left = expression.getLeftExpression(); + PyExpression right = expression.getRightExpression(); + if (left == null || right == null) return null; + + Ref leftTypeRef = getType(left, context); + Ref rightTypeRef = getType(right, context); + if (leftTypeRef == null || rightTypeRef == null) return null; + + PyType intersection = PyIntersectionType.intersection(leftTypeRef.get(), rightTypeRef.get()); + return intersection != null ? Ref.create(intersection) : null; + } + return null; + } + private static @Nullable Ref getNoneType(@NotNull PyExpression typeHint, @NotNull PsiElement resolved) { if (typeHint instanceof PyNoneLiteralExpression || typeHint instanceof PyReferenceExpression && PyNames.NONE.equals(typeHint.getText())) { 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 055d7839b412..d610e5865187 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 @@ -198,6 +198,12 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk return visitUnknownType(); } + @Override + public @NotNull HtmlChunk visitPyIntersectionType(com.jetbrains.python.psi.types.@NotNull PyIntersectionType intersectionType) { + // There is no way to represent intersections through the standard type hints at the moment + return visitUnknownType(); + } + @Override public @NotNull HtmlChunk visitPySelfType(@NotNull PySelfType selfType) { HtmlChunk selfTypeRender = className(isRenderingFqn() ? "typing.Self" : "Self"); //NON-NLS @@ -394,6 +400,11 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk return result.toFragment(); } + @Override + public @NotNull HtmlChunk visitPyIntersectionType(@NotNull PyIntersectionType intersectionType) { + return renderList(ContainerUtil.map(intersectionType.getMembers(), this::render), " & "); + } + private static @Nullable Pair<@NotNull List, @NotNull List> extractLiterals(@NotNull PyUnionType type) { final Collection members = type.getMembers(); @@ -504,6 +515,9 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk case " | " -> { yield styled(separator, PyHighlighter.PY_OPERATION_SIGN); } + case " & " -> { + yield styled(separator, PyHighlighter.PY_OPERATION_SIGN); + } default -> { yield escaped(separator); } 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 7caa9800f2a1..b2b40c80f6bd 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 @@ -431,6 +431,9 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor if (type instanceof PyUnsafeUnionType weakUnionType) { return ContainerUtil.exists(weakUnionType.getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); } + if (type instanceof PyIntersectionType intersectionType) { + return ContainerUtil.exists(intersectionType.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; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java index c60cf89112ef..30f974f4f94d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyFileElementType.java @@ -60,7 +60,7 @@ public class PyFileElementType extends IStubFileElementType { @Override public int getStubVersion() { // Don't forget to update versions of indexes that use the updated stub-based elements - return 106; + return 107; } @Override diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java index 0d99a7c01c52..4ee85c116790 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/stubs/PyTypingAliasStubType.java @@ -53,8 +53,8 @@ public final class PyTypingAliasStubType extends CustomTargetExpressionStubType< (?x) \\s* \\S+(\\[.*])? # initial type like: "list[int]" - (\\s*\\|\\s* # union operator: " | " - \\S+(\\[.*])? # type between union operator + (\\s*[|&]\\s* # union or intersection operators: " | " or " & " + \\S+(\\[.*])? # operand types )* # repeating \\s* """, @@ -174,7 +174,7 @@ public final class PyTypingAliasStubType extends CustomTargetExpressionStubType< @Override public void visitPyBinaryExpression(@NotNull PyBinaryExpression node) { - if (node.getOperator() != PyTokenTypes.OR) { + if (!(node.getOperator() == PyTokenTypes.OR || node.getOperator() == PyTokenTypes.AND)) { illegal[0] = true; return; } 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 f35441356164..2ed44fe0c12f 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 @@ -136,6 +136,9 @@ public final class PyABCUtil { if (type instanceof PyUnsafeUnionType) { return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context)); } + if (type instanceof PyIntersectionType) { + return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context)); + } return false; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCloningTypeVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCloningTypeVisitor.java index 78659cae3bf0..adca766f7de9 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCloningTypeVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyCloningTypeVisitor.java @@ -149,6 +149,11 @@ public abstract class PyCloningTypeVisitor extends PyTypeVisitorExt { return PyUnsafeUnionType.unsafeUnion(ContainerUtil.map(unsafeUnionType.getMembers(), type -> clone(type))); } + @Override + public PyType visitPyIntersectionType(@NotNull PyIntersectionType intersectionType) { + return PyIntersectionType.intersection(ContainerUtil.map(intersectionType.getMembers(), type -> clone(type))); + } + @Override public PyType visitPyTypingNewType(@NotNull PyTypingNewType typingNewType) { return typingNewType; diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyIntersectionType.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyIntersectionType.kt new file mode 100644 index 000000000000..d1e9054a09e5 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyIntersectionType.kt @@ -0,0 +1,103 @@ +package com.jetbrains.python.psi.types + +import com.intellij.openapi.util.NlsSafe +import com.intellij.psi.PsiElement +import com.intellij.util.ProcessingContext +import com.intellij.util.SmartList +import com.jetbrains.python.psi.AccessDirection +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.resolve.PyResolveContext +import com.jetbrains.python.psi.resolve.RatedResolveResult +import org.jetbrains.annotations.ApiStatus +import java.util.Collections + +@ApiStatus.Experimental +class PyIntersectionType private constructor(members: Collection) : PyType { + val members: Set = Collections.unmodifiableSet(LinkedHashSet(members)) + + override fun resolveMember( + name: String, + location: PyExpression?, + direction: AccessDirection, + resolveContext: PyResolveContext, + ): List? { + val ret = SmartList() + var allNulls = true + for (member in members) { + if (member != null) { + val result = member.resolveMember(name, location, direction, resolveContext) + if (result != null) { + allNulls = false + ret.addAll(result) + } + } + } + return if (allNulls) null else ret + } + + override fun getCompletionVariants(completionPrefix: String?, location: PsiElement?, context: ProcessingContext?): Array { + return members.flatMap { it?.getCompletionVariants(completionPrefix, location, context)?.asList() ?: emptyList() } + .distinct() + .toTypedArray() + } + + override fun getName(): @NlsSafe String { + return members.joinToString(separator = " & ") { it?.name ?: "Any" } + } + + override fun isBuiltin(): Boolean { + return members.all { it != null && it.isBuiltin } + } + + override fun assertValid(message: String?) { + for (member in members) { + member?.assertValid(message) + } + } + + override fun acceptTypeVisitor(visitor: PyTypeVisitor): T { + if (visitor is PyTypeVisitorExt) { + return visitor.visitPyIntersectionType(this) + } + return visitor.visitPyType(this) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PyIntersectionType + + return members == other.members + } + + override fun hashCode(): Int { + return members.hashCode() + } + + override fun toString(): String { + return "PyIntersectionType: $name" + } + + companion object { + @JvmStatic + fun intersection(vararg types: PyType?): PyType? { + return intersection(types.toList()) + } + + @JvmStatic + fun intersection(types: Collection): PyType? { + val newMembers = buildSet { + for (member in types) { + if (member is PyIntersectionType) { + addAll(member.members) + } + else { + add(member) + } + } + } + return if (newMembers.size > 1) PyIntersectionType(newMembers) else newMembers.firstOrNull() + } + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyRecursiveTypeVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyRecursiveTypeVisitor.java index 3e8d362b642c..68281688e72c 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyRecursiveTypeVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyRecursiveTypeVisitor.java @@ -189,6 +189,11 @@ public final class PyRecursiveTypeVisitor extends PyTypeVisitorExt(unsafeUnionType.getMembers())); } + @Override + public @NotNull List<@Nullable PyType> visitPyIntersectionType(@NotNull PyIntersectionType intersectionType) { + return Collections.unmodifiableList(new ArrayList<>(intersectionType.getMembers())); + } + @Override public @NotNull List<@Nullable PyType> visitPyUnpackedTupleType(@NotNull PyUnpackedTupleType unpackedTupleType) { return unpackedTupleType.getElementTypes(); 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 17db3c1f6687..641fb24ed763 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 @@ -189,6 +189,14 @@ public final class PyTypeChecker { return Optional.of(match(weakUnionType, actual, context)); } + if (actual instanceof PyIntersectionType intersectionType) { + return Optional.of(match(expected, intersectionType, context)); + } + + if (expected instanceof PyIntersectionType intersectionType) { + return Optional.of(match(intersectionType, actual, context)); + } + if (expected instanceof PyClassType && actual instanceof PyClassType) { Optional match = match((PyClassType)expected, (PyClassType)actual, context); if (match.isPresent()) { @@ -521,10 +529,6 @@ public final class PyTypeChecker { return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false)); } - private static boolean match(@NotNull PyType expected, @NotNull PyUnsafeUnionType actual, @NotNull MatchContext context) { - return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false)); - } - private static @NotNull Optional match(@NotNull PyTupleType expected, @NotNull PyUnionType actual, @NotNull MatchContext context) { @@ -547,6 +551,18 @@ public final class PyTypeChecker { return ContainerUtil.or(expected.getMembers(), type -> match(type, actual, context).orElse(true)); } + private static boolean match(@NotNull PyType expected, @NotNull PyIntersectionType actual, @NotNull MatchContext context) { + return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false)); + } + + private static boolean match(@NotNull PyIntersectionType expected, @NotNull PyType actual, @NotNull MatchContext context) { + return ContainerUtil.all(expected.getMembers(), type -> match(type, actual, context).orElse(true)); + } + + private static boolean match(@NotNull PyType expected, @NotNull PyUnsafeUnionType actual, @NotNull MatchContext context) { + return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false)); + } + private static boolean match(@NotNull PyUnsafeUnionType expected, @NotNull PyType actual, @NotNull MatchContext context) { if (expected.getMembers().contains(actual)) { return true; @@ -1141,6 +1157,9 @@ public final class PyTypeChecker { if (type instanceof PyUnsafeUnionType weakUnion) { return ContainerUtil.exists(weakUnion.getMembers(), member -> isUnknown(member, genericsAreUnknown, context)); } + if (type instanceof PyIntersectionType intersectionType) { + return ContainerUtil.exists(intersectionType.getMembers(), member -> isUnknown(member, genericsAreUnknown, context)); + } return false; } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java index cc72a729ec7f..5304fb8cb45b 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeUtil.java @@ -160,6 +160,9 @@ public final class PyTypeUtil { if (type instanceof PyUnsafeUnionType weakUnionType) { return StreamEx.of(weakUnionType.getMembers()); } + if (type instanceof PyIntersectionType intersectionType) { + return StreamEx.of(intersectionType.getMembers()); + } return StreamEx.of(type); } @@ -221,6 +224,11 @@ public final class PyTypeUtil { return Collectors.collectingAndThen(Collectors.toList(), PyUnsafeUnionType::unsafeUnion); } + @ApiStatus.Experimental + public static @NotNull Collector<@Nullable PyType, ?, @Nullable PyType> toIntersection() { + return Collectors.collectingAndThen(Collectors.toList(), PyIntersectionType::intersection); + } + public static @NotNull Collector<@Nullable PyType, ?, @Nullable PyType> toUnion(@Nullable PyType streamSource) { return toUnion(streamSource instanceof PyUnsafeUnionType ? PyUnsafeUnionType::unsafeUnion : PyUnionType::union); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeVisitorExt.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeVisitorExt.java index bbe5572cffcc..d8ea0d06ce3c 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeVisitorExt.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeVisitorExt.java @@ -47,6 +47,10 @@ public abstract class PyTypeVisitorExt extends PyTypeVisitor { return visitPyType(unsafeUnionType); } + public T visitPyIntersectionType(@NotNull PyIntersectionType intersectionType) { + return visitPyType(intersectionType); + } + public T visitPyTypingNewType(@NotNull PyTypingNewType typingNewType) { return visitPyClassType(typingNewType); } diff --git a/python/testData/inspections/PyTypeCheckerInspection/IntersectionImplicitProtocolMatching.py b/python/testData/inspections/PyTypeCheckerInspection/IntersectionImplicitProtocolMatching.py new file mode 100644 index 000000000000..3f29fd50eda1 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/IntersectionImplicitProtocolMatching.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_intersection_members_match_no_any(iterable: "A & B"): + for _ in iterable: + pass + + +def some_intersection_members_match_no_any(iterable: "A & B & None"): + for _ in iterable: + pass + + +def all_intersection_members_dont_match_no_any(iterable: "C & None"): + for _ in iterable: + pass + + +def all_intersection_members_match_with_any(iterable: "A & B & Any"): + for _ in iterable: + pass + + +def some_intersection_members_match_with_any(iterable: "A & B & None & Any"): + for _ in iterable: + pass + + +def all_intersection_members_dont_match_with_any(iterable: "C & None & Any"): + for _ in iterable: + pass diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/intersectionMemberAttributeAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/intersectionMemberAttributeAccess.py new file mode 100644 index 000000000000..c7e7923a9275 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/intersectionMemberAttributeAccess.py @@ -0,0 +1,25 @@ +from typing import Any + + +class A: + def method(self): + pass + + +class B: + def method(self): + pass + + +# Using string annotations to suppress the warnings about unresolved '__and__' in type +def intersection_with_all_compatible_types(x: 'A & B'): + x.method() + +def intersection_with_some_incompatible_types(x: 'A & None'): + x.method() + +def intersection_with_all_incompatible_types(x: 'object & None'): + x.method() + +def intersection_with_incompatible_types_and_any(x: 'Any & None'): + x.method() diff --git a/python/testData/inspections/unusedImport/annotationWithQuotedIntersectionUsesImportsFromTyping/annotationWithQuotedIntersectionUsesImportsFromTyping.py b/python/testData/inspections/unusedImport/annotationWithQuotedIntersectionUsesImportsFromTyping/annotationWithQuotedIntersectionUsesImportsFromTyping.py new file mode 100644 index 000000000000..5236db8df3fa --- /dev/null +++ b/python/testData/inspections/unusedImport/annotationWithQuotedIntersectionUsesImportsFromTyping/annotationWithQuotedIntersectionUsesImportsFromTyping.py @@ -0,0 +1,3 @@ +from typing import Any, Optional + +x: "str & Any" \ No newline at end of file diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable.py new file mode 100644 index 000000000000..ea67e4af83c2 --- /dev/null +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable.py @@ -0,0 +1,2 @@ +x: str & int +var = x \ No newline at end of file diff --git a/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable_after.py b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable_after.py new file mode 100644 index 000000000000..1fbfb7e88d3c --- /dev/null +++ b/python/testData/intentions/PyAnnotateVariableTypeIntentionTest/intersectionTypeIsNotDenotable_after.py @@ -0,0 +1,4 @@ +from typing import Any + +x: str & int +var: [Any] = x \ No newline at end of file diff --git a/python/testData/quickdoc/IntersectionType.html b/python/testData/quickdoc/IntersectionType.html new file mode 100644 index 000000000000..50c2e50cb28a --- /dev/null +++ b/python/testData/quickdoc/IntersectionType.html @@ -0,0 +1 @@ +
var: int & str
\ No newline at end of file diff --git a/python/testData/quickdoc/IntersectionType.py b/python/testData/quickdoc/IntersectionType.py new file mode 100644 index 000000000000..9610b902c747 --- /dev/null +++ b/python/testData/quickdoc/IntersectionType.py @@ -0,0 +1 @@ +var: int & str \ No newline at end of file diff --git a/python/testData/stubs/TypeAliasStubs.py b/python/testData/stubs/TypeAliasStubs.py index c286d0971ced..cae7444760e0 100644 --- a/python/testData/stubs/TypeAliasStubs.py +++ b/python/testData/stubs/TypeAliasStubs.py @@ -19,7 +19,7 @@ S6_notOk = b"int" bin1_ok = int | str bin2_ok = int | str | bool | None bin3_ok = Union[str, bool] | None -bin4_notOk = str & int +bin4_ok = str & int list_notOk = [int, str] bin5_notOk: int | str = "foo" diff --git a/python/testData/types/LegacyTypeAliasesWithQuotedIntersectionTypesPreservedInStubs/mod.py b/python/testData/types/LegacyTypeAliasesWithQuotedIntersectionTypesPreservedInStubs/mod.py new file mode 100644 index 000000000000..2cb4df4f8da6 --- /dev/null +++ b/python/testData/types/LegacyTypeAliasesWithQuotedIntersectionTypesPreservedInStubs/mod.py @@ -0,0 +1,3 @@ +Alias = list['int & str'] + +x: Alias \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java index fc4ce2dbf56e..b4ccba3035ec 100644 --- a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java +++ b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java @@ -879,6 +879,11 @@ public class Py3QuickDocTest extends LightMarkedTestCase { }); } + // PY-76922 + public void testIntersectionType() { + checkHTMLOnly(); + } + @Override protected String getTestDataPath() { return super.getTestDataPath() + "/quickdoc/"; diff --git a/python/testSrc/com/jetbrains/python/PyTypingTest.java b/python/testSrc/com/jetbrains/python/PyTypingTest.java index a9426bdae603..753eaedadc54 100644 --- a/python/testSrc/com/jetbrains/python/PyTypingTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypingTest.java @@ -6877,6 +6877,22 @@ public class PyTypingTest extends PyTestCase { """); } + // PY-76922 + public void testIntersectionTypeParsing() { + doTest("int & str", """ + expr: int & str + """); + } + + // PY-76922 + public void testLegacyTypeAliasesWithQuotedIntersectionTypesPreservedInStubs() { + doMultiFileStubAwareTest("list[int & str]", """ + from mod import x + + expr = x + """); + } + private void doTestNoInjectedText(@NotNull String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject()); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java index 37adb1ef707a..adead4d435ab 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java @@ -3139,6 +3139,11 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { doTest(); } + // PY-76922 + public void testIntersectionImplicitProtocolMatching() { + doTest(); + } + // PY-76822 public void testProtocolWithAssignedPropertyInMethod() { doTestByText(""" @@ -4174,5 +4179,39 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase { async_for([1, 2, 3]) """); } + + // PY-76922 + public void testIntersectionType() { + doTestByText(""" + int_and_str: int & str + str_and_int: int & str + int_or_str: int | str + + n: int = int_and_str + s: str = int_and_str + + int_and_str = n + int_and_str = s + + int_or_str = int_and_str + int_and_str = int_or_str + + str_and_int = int_and_str + int_and_str = str_and_int + + class A: pass + class B: pass + class C(A, B): pass + + a_and_b: A & B + a_and_b = A() + a_and_b = B() + a_and_b = C() + + a: A = a_and_b + b: B = a_and_b + c: C = a_and_b + """); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index 22409789fe60..df63174e4acb 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -561,4 +561,9 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase super().non_existing() """); } + + // PY-76922 + public void testIntersectionMemberAttributeAccess() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnusedImportTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnusedImportTest.java index 0452e7b77ace..3f11a7a7409d 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnusedImportTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnusedImportTest.java @@ -98,6 +98,11 @@ public class PyUnusedImportTest extends PyTestCase { runWithLanguageLevel(LanguageLevel.PYTHON34, this::doTest); } + // PY-76922 + public void testAnnotationWithQuotedIntersectionUsesImportsFromTyping() { + doTest(); + } + public void testSuppressedForUnreachableCode() { doTest(); } diff --git a/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java index c807755ee50c..f74d830ea94e 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyAnnotateVariableTypeIntentionTest.java @@ -336,6 +336,11 @@ public class PyAnnotateVariableTypeIntentionTest extends PyIntentionTestCase { doAnnotationTest(); } + // PY-76922 + public void testIntersectionTypeIsNotDenotable() { + doAnnotationTest(); + } + private void doAnnotationTest() { doTest(LanguageLevel.getLatest()); }