diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/types/PyNeverType.kt b/python/python-psi-api/src/com/jetbrains/python/psi/types/PyNeverType.kt new file mode 100644 index 000000000000..13067098759b --- /dev/null +++ b/python/python-psi-api/src/com/jetbrains/python/psi/types/PyNeverType.kt @@ -0,0 +1,43 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.psi.types + +import com.intellij.psi.PsiElement +import com.intellij.util.ProcessingContext +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 + +class PyNeverType private constructor(private val name: String): PyType { + companion object { + @JvmField val NEVER: PyNeverType = PyNeverType("Never") + @JvmField val NO_RETURN: PyNeverType = PyNeverType("NoReturn") + + @JvmStatic + fun PyType?.toNoReturnIfNeeded(): PyType? = if (this === NEVER) NO_RETURN else this + } + + override fun getName(): String = name + override fun isBuiltin(): Boolean = true + override fun assertValid(message: String?) {} + + override fun equals(other: Any?): Boolean = other is PyNeverType + override fun hashCode(): Int = name.hashCode() + + override fun resolveMember( + name: String, + location: PyExpression?, + direction: AccessDirection, + resolveContext: PyResolveContext, + ): List = emptyList() + + override fun getCompletionVariants( + completionPrefix: String?, + location: PsiElement?, + context: ProcessingContext?, + ): Array = emptyArray() + + override fun acceptTypeVisitor(visitor: PyTypeVisitor): T { + return visitor.visitPyNeverType(this) + } +} \ No newline at end of file diff --git a/python/python-psi-api/src/com/jetbrains/python/psi/types/PyTypeVisitor.java b/python/python-psi-api/src/com/jetbrains/python/psi/types/PyTypeVisitor.java index 0a74502a3a78..fae195586513 100644 --- a/python/python-psi-api/src/com/jetbrains/python/psi/types/PyTypeVisitor.java +++ b/python/python-psi-api/src/com/jetbrains/python/psi/types/PyTypeVisitor.java @@ -74,6 +74,10 @@ public abstract class PyTypeVisitor { public T visitPyCallableParameterListType(@NotNull PyCallableParameterListType callableParameterListType) { return visitPyType(callableParameterListType); } + + public T visitPyNeverType(@NotNull PyNeverType neverType) { + return visitPyType(neverType); + } public T visitUnknownType() { return null; diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt index d72a1f9a5601..fd48a389da4c 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/CallInstruction.kt @@ -2,10 +2,10 @@ package com.jetbrains.python.codeInsight.controlflow import com.intellij.codeInsight.controlflow.ControlFlowBuilder import com.intellij.codeInsight.controlflow.impl.InstructionImpl -import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider import com.jetbrains.python.psi.PyCallExpression import com.jetbrains.python.psi.PyFunction import com.jetbrains.python.psi.resolve.PyResolveContext +import com.jetbrains.python.psi.types.PyNeverType import com.jetbrains.python.psi.types.TypeEvalContext class CallInstruction(builder: ControlFlowBuilder, call: PyCallExpression) : InstructionImpl(builder, call) { @@ -18,7 +18,7 @@ class CallInstruction(builder: ControlFlowBuilder, call: PyCallExpression) : Ins if (callees.size == 1) { val pyFunction = callees.single() if (pyFunction is PyFunction) { - return PyTypingTypeProvider.isNoReturn(pyFunction, context) + return context.getReturnType(pyFunction) is PyNeverType } } return false diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java index 207e0dd4a0a4..bbe5d813a289 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/controlflow/PyTypeAssertionEvaluator.java @@ -219,7 +219,7 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor { return Ref.create(excludeFromUnion(unionType, suggested, context)); } if (match(suggested, initial, context)) { - return Ref.create(PyNeverType.INSTANCE); + return Ref.create(PyNeverType.NEVER); } Ref<@Nullable PyType> diff = trySubtract(initial, suggested, context); return diff != null ? diff : Ref.create(initial); @@ -240,7 +240,7 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor { } } if (members.isEmpty()) { - return PyNeverType.INSTANCE; + return PyNeverType.NEVER; } return PyUnionType.union(members); } @@ -261,7 +261,7 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor { List enumMembers = PyStdlibTypeProvider.getEnumMembers(classType1.getPyClass(), context).toList(); List filteredEnumMembers = ContainerUtil.filter(enumMembers, m -> !PyTypeChecker.match(type2, m, context)); if (filteredEnumMembers.isEmpty()) { - return Ref.create(PyNeverType.INSTANCE); + return Ref.create(PyNeverType.NEVER); } PyType type = enumMembers.size() == filteredEnumMembers.size() ? type1 : PyUnionType.union(filteredEnumMembers); return Ref.create(type); 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 1cbb51abfc8c..ef8fb38fcee9 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 @@ -321,6 +321,12 @@ public final class PyTypeHintGenerationUtil { return Traversal.CONTINUE; } + @Override + public @NotNull Traversal visitPyNeverType(@NotNull PyNeverType neverType) { + typingTypes.add(neverType.getName()); + return Traversal.CONTINUE; + } + @Override public @NotNull Traversal visitPyNamedTupleType(@NotNull PyNamedTupleType namedTupleType) { final PyQualifiedNameOwner element = namedTupleType.getDeclarationElement(); 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 12133a838d77..924249858f4e 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 @@ -1270,12 +1270,6 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< typeHintedWithName(owner, context, CLASS_VAR)); } - public static boolean isNoReturn(@NotNull PyFunction function, @NotNull TypeEvalContext context) { - return PyUtil.getParameterizedCachedValue(function, context, p -> - typeHintedWithName(function, context, NO_RETURN, NO_RETURN_EXT, NEVER, NEVER_EXT)); - } - - private static boolean resolvesToQualifiedNames(@NotNull PyExpression expression, @NotNull TypeEvalContext context, String... names) { final var qualifiedNames = resolveToQualifiedNames(expression, context); return ContainerUtil.exists(names, qualifiedNames::contains); @@ -1465,8 +1459,11 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< private static @Nullable PyType getNeverType(@NotNull PsiElement element) { var qName = getQualifiedName(element); if (qName == null) return null; - if (List.of(NEVER, NEVER_EXT, NO_RETURN, NO_RETURN_EXT).contains(qName)) { - return PyNeverType.INSTANCE; + if (List.of(NEVER, NEVER_EXT).contains(qName)) { + return PyNeverType.NEVER; + } + if (List.of(NO_RETURN, NO_RETURN_EXT).contains(qName)) { + return PyNeverType.NO_RETURN; } return null; } 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 c46afe0e11e8..8708a7564d9a 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 @@ -229,6 +229,11 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk return result.toFragment(); } + @Override + public @NotNull HtmlChunk visitPyNeverType(@NotNull PyNeverType neverType) { + return className(neverType.getName()); + } + @Override public HtmlChunk visitPyUnionType(@NotNull PyUnionType unionType) { // TODO Exclude "Unknown" once it's introduced, don't exclude explicit typing.Any diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java index 2f3f6765ff78..98af498d5833 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyTypeCheckerInspection.java @@ -333,14 +333,15 @@ public class PyTypeCheckerInspection extends PyInspection { } } - if (PyUtil.isInitMethod(node) && !(returnsNone || PyTypingTypeProvider.isNoReturn(node, myTypeEvalContext))) { + final PyType annotatedType = myTypeEvalContext.getReturnType(node); + + if (PyUtil.isInitMethod(node) && !(returnsNone || annotatedType instanceof PyNeverType)) { registerProblem(annotation != null ? annotation.getValue() : node.getTypeComment(), PyPsiBundle.message("INSP.type.checker.init.should.return.none")); } if (node.isGenerator()) { boolean shouldBeAsync = node.isAsync() && node.isAsyncAllowed(); - final PyType annotatedType = myTypeEvalContext.getReturnType(node); final var generatorDesc = GeneratorTypeDescriptor.create(annotatedType); if (generatorDesc != null && generatorDesc.isAsync() != shouldBeAsync) { final PyType inferredType = node.getInferredReturnType(myTypeEvalContext); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.kt index 9b6dc709d7df..a35b12008db4 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.kt @@ -844,7 +844,7 @@ private fun PyClassType.resolveConstructors(callSite: PyCallSiteExpression?, res val callType = if (callSite != null) callableType.getCallType(context, callSite) else callableType.getReturnType(context) val expectedType = toInstance() PyTypeUtil.toStream(callType).anyMatch { - it == null || !PyTypeChecker.match(expectedType, it, context) + it == null || it is PyNeverType || !PyTypeChecker.match(expectedType, it, context) } } else false diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCapturePatternImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCapturePatternImpl.java index 61b685654303..e575a496d37d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCapturePatternImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCapturePatternImpl.java @@ -125,7 +125,7 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt PyType mappingType = PyTypeUtil.convertToType(type, "typing.Mapping", pattern, context); if (mappingType == null) { - return PyNeverType.INSTANCE; + return PyNeverType.NEVER; } else if (mappingType instanceof PyCollectionType collectionType) { return collectionType.getElementTypes().get(1); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java index 94a0329ea40b..6b19f5492c2f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyFunctionImpl.java @@ -172,6 +172,7 @@ public class PyFunctionImpl extends PyBaseElementImpl implements inferredType = returnType; } } + inferredType = PyNeverType.toNoReturnIfNeeded(inferredType); return PyTypingTypeProvider.removeNarrowedTypeIfNeeded(PyTypingTypeProvider.toAsyncIfNeeded(this, inferredType)); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNeverType.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNeverType.kt deleted file mode 100644 index 66cb3e501728..000000000000 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyNeverType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.jetbrains.python.psi.types - -object PyNeverType: PyUnionType(LinkedHashSet()) { - override fun getName(): String = "Never" -} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTupleType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTupleType.java index 239b5a2e51b5..edc365f4614d 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTupleType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTupleType.java @@ -113,7 +113,7 @@ public class PyTupleType extends PyClassTypeImpl implements PyCollectionType { return type; } }); - return PyUnionType.union(unpackedTypes); + return PyUnionType.unionOrNever(unpackedTypes); } public @NotNull PyUnpackedTupleType asUnpackedTupleType() { 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 600c180ffb0a..ba8482c159e0 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 @@ -113,14 +113,6 @@ public final class PyTypeChecker { } } - if (actual instanceof PyNeverType) { - return Optional.of(true); - } - - if (expected instanceof PyNeverType) { - return Optional.of(false); - } - if (expected instanceof PyClassType) { Optional match = matchObject((PyClassType)expected, actual); if (match.isPresent()) { @@ -172,6 +164,14 @@ public final class PyTypeChecker { return Optional.of(match(callableParameterListType, actual, context)); } + if (actual instanceof PyNeverType) { + return Optional.of(true); + } + + if (expected instanceof PyNeverType) { + return Optional.of(false); + } + if (actual instanceof PyUnionType) { return Optional.of(match(expected, (PyUnionType)actual, context)); } diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyUnionType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyUnionType.java index 1d534103b7a1..9158b0e71fd3 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyUnionType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyUnionType.java @@ -86,23 +86,44 @@ public class PyUnionType implements PyType { return union(Arrays.asList(type1, type2)); } + /** + * Constructs a union of the given types. + *

+ * If the resulting union would be empty, returns {@code null} (representing Any type). + * Consider using {@link #unionOrNever} instead, which falls back to {@link PyNeverType#NEVER}. + * + * @param members a collection of types to union + * @return a PyType representing the union, or null if no valid members + */ public static @Nullable PyType union(@NotNull Collection<@Nullable PyType> members) { - if (members.size() < 2) { - return ContainerUtil.getFirstItem(members); - } - else { - final LinkedHashSet newMembers = new LinkedHashSet<>(); - for (PyType member : members) { - if (member instanceof PyUnionType) { - newMembers.addAll(((PyUnionType)member).getMembers()); - } - else { - newMembers.add(member); - } - } + return unionOrDefault(members, null); + } - return newMembers.size() < 2 ? ContainerUtil.getFirstItem(newMembers) : new PyUnionType(newMembers); + /** + * Constructs a union of the given types, falling back to {@link PyNeverType#NEVER} instead of null (Any). + * + * @param members a collection of types to union + * @return a PyType representing the union, or {@link PyNeverType#NEVER} if no valid members + */ + public static @Nullable PyType unionOrNever(@NotNull Collection<@Nullable PyType> members) { + return unionOrDefault(members, PyNeverType.NEVER); + } + + private static @Nullable PyType unionOrDefault(@NotNull Collection<@Nullable PyType> members, @Nullable PyType defaultResult) { + final LinkedHashSet newMembers = new LinkedHashSet<>(); + for (PyType member : members) { + if (member instanceof PyNeverType) { + defaultResult = PyNeverType.NEVER; + continue; + } + if (member instanceof PyUnionType) { + newMembers.addAll(((PyUnionType)member).getMembers()); + } + else { + newMembers.add(member); + } } + return newMembers.size() < 2 ? ContainerUtil.getFirstItem(newMembers, defaultResult) : new PyUnionType(newMembers); } public static @Nullable PyType createWeakType(@Nullable PyType type) { @@ -140,8 +161,8 @@ public class PyUnionType implements PyType { /** * Excludes all subtypes of type from the union * - * @param type type to exclude. If type is a union all subtypes of union members will be excluded from the union - * If type is null only null will be excluded from the union. + * @param type type to exclude. If type is a union all subtypes of union members will be excluded from the union + * If type is null only null will be excluded from the union. * @return union with excluded types */ public @Nullable PyType exclude(@Nullable PyType type, @NotNull TypeEvalContext context) { diff --git a/python/testData/quickdoc/NeverType.html b/python/testData/quickdoc/NeverType.html new file mode 100644 index 000000000000..16d3b28b70b5 --- /dev/null +++ b/python/testData/quickdoc/NeverType.html @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/python/testData/quickdoc/NeverType.py b/python/testData/quickdoc/NeverType.py new file mode 100644 index 000000000000..137ba1a21bf1 --- /dev/null +++ b/python/testData/quickdoc/NeverType.py @@ -0,0 +1,3 @@ +from typing import Never + +x: Never diff --git a/python/testData/typing/ignored.txt b/python/testData/typing/ignored.txt index 16c81257ebdf..c0420865c496 100644 --- a/python/testData/typing/ignored.txt +++ b/python/testData/typing/ignored.txt @@ -14,7 +14,6 @@ classes_override.py constructors_call_init.py constructors_call_new.py constructors_call_type.py -constructors_call_metaclass.py constructors_callable.py dataclasses_hash.py dataclasses_inheritance.py @@ -64,5 +63,4 @@ specialtypes_promotions.py specialtypes_type.py tuples_type_form.py tuples_unpacked.py -tuples_type_compat.py typeddicts_readonly_inheritance.py \ 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 bf380f72b2c8..9c380a1f2199 100644 --- a/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java +++ b/python/testSrc/com/jetbrains/python/Py3QuickDocTest.java @@ -846,6 +846,11 @@ public class Py3QuickDocTest extends LightMarkedTestCase { public void testTypeAliasStatement() { checkHTMLOnly(); } + + // PY-78119 + public void testNeverType() { + checkHTMLOnly(); + } // PY-23067 public void testFunctoolsWraps() {