mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90942 typing: introduce PyTopType
(cherry picked from commit 219d3a69024c41a3ff0cc78ae3588dfa6c6982aa) GitOrigin-RevId: 271d70a890524b0a8d8450550b080e26787e3327
This commit is contained in:
committed by
intellij-monorepo-bot
parent
47900480f0
commit
7c666d14f6
@@ -0,0 +1,59 @@
|
||||
// Copyright 2000-2026 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.PyNames
|
||||
import com.jetbrains.python.psi.AccessDirection
|
||||
import com.jetbrains.python.psi.PyExpression
|
||||
import com.jetbrains.python.psi.PyPsiFacade
|
||||
import com.jetbrains.python.psi.resolve.PyResolveContext
|
||||
import com.jetbrains.python.psi.resolve.RatedResolveResult
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* The top type of the Python type hierarchy: the supertype of every type, equivalent to `builtins.object`.
|
||||
*
|
||||
* There is only ever one top type, so this is a singleton. Unlike a [PyClassType] wrapping `object`, it needs no
|
||||
* anchor [PsiElement] to be produced.
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
object PyTopType : PyType {
|
||||
override val name: String = "object"
|
||||
|
||||
override val isBuiltin: Boolean = true
|
||||
|
||||
override fun assertValid(message: String?) {}
|
||||
|
||||
override fun resolveMember(
|
||||
name: String,
|
||||
location: PyExpression?,
|
||||
direction: AccessDirection,
|
||||
resolveContext: PyResolveContext,
|
||||
): List<RatedResolveResult> =
|
||||
objectType(location)?.resolveMember(name, location, direction, resolveContext).orEmpty()
|
||||
|
||||
override fun getCompletionVariants(
|
||||
completionPrefix: String?,
|
||||
location: PsiElement,
|
||||
context: ProcessingContext,
|
||||
): Array<out Any> =
|
||||
objectType(location)?.getCompletionVariants(completionPrefix, location, context).orEmpty()
|
||||
|
||||
override fun getAllMembers(resolveContext: PyResolveContext): List<PyTypeMember> =
|
||||
objectType(resolveContext.typeEvalContext.origin)?.getAllMembers(resolveContext).orEmpty()
|
||||
|
||||
override fun findMember(name: String, resolveContext: PyResolveContext): List<PyTypeMember> =
|
||||
objectType(resolveContext.typeEvalContext.origin)?.findMember(name, resolveContext).orEmpty()
|
||||
|
||||
private fun objectType(anchor: PsiElement?): PyClassType? {
|
||||
if (anchor == null) return null
|
||||
val facade = PyPsiFacade.getInstance(anchor.project)
|
||||
val objectClass = facade.createClassByQName(PyNames.OBJECT, anchor) ?: return null
|
||||
return facade.createClassType(objectClass, false)
|
||||
}
|
||||
|
||||
override fun <T> acceptTypeVisitor(visitor: PyTypeVisitor<T>): T? = visitor.visitPyTopType(this)
|
||||
|
||||
override fun toString(): String = name
|
||||
}
|
||||
@@ -73,6 +73,10 @@ abstract class PyTypeVisitor<T> {
|
||||
return visitPyType(neverType)
|
||||
}
|
||||
|
||||
open fun visitPyTopType(topType: PyTopType): T? {
|
||||
return visitPyType(topType)
|
||||
}
|
||||
|
||||
open fun visitAnyType(): T? {
|
||||
return if (PyAnyType.isEnabled) visitPyType(PyAnyType.Any) else null
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.jetbrains.python.psi.types.PyNeverType;
|
||||
import com.jetbrains.python.psi.types.PyOverloadType;
|
||||
import com.jetbrains.python.psi.types.PyParamSpecType;
|
||||
import com.jetbrains.python.psi.types.PySelfType;
|
||||
import com.jetbrains.python.psi.types.PyTopType;
|
||||
import com.jetbrains.python.psi.types.PyTupleType;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.PyTypeParameterType;
|
||||
@@ -418,6 +419,11 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk
|
||||
return className(neverType.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HtmlChunk visitPyTopType(@NotNull PyTopType topType) {
|
||||
return className(isRenderingFqn() ? PyNames.FQN.OBJECT : topType.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HtmlChunk visitPyUnionType(@NotNull PyUnionType unionType) {
|
||||
// TODO Exclude "Unknown" once it's introduced, don't exclude explicit typing.Any
|
||||
|
||||
@@ -71,11 +71,30 @@ class PyIntersectionType private constructor(members: Collection<PyType?>) : PyC
|
||||
return intersection(types.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an intersection of the given types.
|
||||
*
|
||||
* If the resulting intersection would be empty, returns [PyTopType], which is the natural colapse of an intersection.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun intersection(types: Collection<PyType?>): PyType? {
|
||||
return intersectionOrDefault(types, PyTopType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an intersection of the given types, falling back to Unknown instead of [PyTopType].
|
||||
*
|
||||
* An intersection of no types is the type that constrains nothing, i.e. the top type.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun intersectionOrUnknown(types: Collection<PyType?>): PyType? {
|
||||
return intersectionOrDefault(types, PyAnyType.unknown)
|
||||
}
|
||||
|
||||
private fun intersectionOrDefault(types: Collection<PyType?>, defaultResult: PyType?): PyType? {
|
||||
val newMembers = buildSet {
|
||||
for (member in types) {
|
||||
if (member is PyNeverType) return@intersection member
|
||||
if (member is PyNeverType) return member
|
||||
if (member is PyIntersectionType) {
|
||||
addAll(member.members)
|
||||
}
|
||||
@@ -84,7 +103,11 @@ class PyIntersectionType private constructor(members: Collection<PyType?>) : PyC
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (newMembers.size > 1) PyIntersectionType(newMembers) else newMembers.firstOrNull()
|
||||
return when {
|
||||
newMembers.size > 1 -> PyIntersectionType(newMembers)
|
||||
newMembers.isEmpty() -> defaultResult
|
||||
else -> newMembers.single()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,12 @@ object PyTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
// `PyTopType` is the top type `object`: every type is assignable to it, just like `matchObject` accepts
|
||||
// any `actual` when `expected` is the `object` class.
|
||||
if (expected is PyTopType) {
|
||||
return Optional.of(true)
|
||||
}
|
||||
|
||||
if (actual is PyNarrowedType && expected is PyNarrowedType) {
|
||||
return match(expected, actual, context)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ import com.jetbrains.python.psi.types.PyRecursiveTypeVisitor.PyTypeTraverser
|
||||
import com.jetbrains.python.psi.types.PyTypeChecker.convertToType
|
||||
import com.jetbrains.python.psi.types.PyTypeChecker.findGenericDefinitionType
|
||||
import com.jetbrains.python.psi.types.PyTypeChecker.match
|
||||
import com.jetbrains.python.psi.types.PyTypeUtil.createTupleOfLiteralStringsType
|
||||
import com.jetbrains.python.psi.types.PyTypeUtil.extractStringLiteralsFromTupleType
|
||||
import com.jetbrains.python.psi.types.PyTypeUtil.widenLiteralAndNumeric
|
||||
import one.util.streamex.StreamEx
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -160,11 +158,13 @@ object PyTypeUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type creates a stream of all its members if it's a union type or of only the type itself otherwise.
|
||||
* Given a type creates a stream of all its members if it's a [PyCompositeType] or of only the type itself otherwise.
|
||||
*
|
||||
*
|
||||
* It allows to process types received as the result of multiresolve uniformly with the others.
|
||||
* It lets a composite type (e.g. the union produced when a reference resolves to several declarations) be processed
|
||||
* uniformly with a non-composite one.
|
||||
*/
|
||||
@Deprecated("Use the high level composite api instead `PyType.compositeX`")
|
||||
@JvmStatic
|
||||
fun PyType?.toStream(): StreamEx<PyType?> =
|
||||
if (this is PyCompositeType)
|
||||
@@ -275,14 +275,106 @@ object PyTypeUtil {
|
||||
}
|
||||
}
|
||||
|
||||
val PyType?.components: List<PyType?>
|
||||
val PyType?.compositeComponents: List<PyType?>
|
||||
@ApiStatus.Experimental
|
||||
get() = if (this is PyCompositeType) members.toList() else listOf(this)
|
||||
|
||||
val PyType?.componentSequence: Sequence<PyType?>
|
||||
val PyType?.compositeComponentSequence: Sequence<PyType?>
|
||||
@ApiStatus.Experimental
|
||||
get() = if (this is PyCompositeType) members.asSequence() else sequenceOf(this)
|
||||
|
||||
/**
|
||||
* Decomposes [this] into the list of its members (or a single-element list for a non-composite type),
|
||||
* hands that list to [transform], and reassembles the result into a composite type *of the same kind*
|
||||
* ([PyUnionType], [PyUnsafeUnionType] or [PyIntersectionType]); a non-composite type is rebuilt as a union,
|
||||
* which collapses a single remaining member back to itself.
|
||||
*
|
||||
* It lets one uniformly deconstruct a composite type, process its members and build it back without branching
|
||||
* on the concrete composite kind:
|
||||
* ```
|
||||
* val transformed = someType.compositeTransform { members ->
|
||||
* members.map { ... }.filter { ... }.mapNotNull { ... }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If [transform] returns an empty list, the result is the identity element of the composite kind:
|
||||
* `Never` for a union or an unsafe union (bottom), and `object` for an intersection (top). The `object` type is
|
||||
* resolved against [this]'s original members and degrades to [PyAnyType.unknown] only for an anchorless intersection
|
||||
* (one made solely of `Any`/`None`).
|
||||
*
|
||||
* @see compositeMap
|
||||
* @see compositeMatchesAsSubtype
|
||||
* @see compositeMatchesAsSupertype
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
fun PyType?.compositeTransform(transform: (List<PyType?>) -> List<PyType?>): PyType? =
|
||||
rebuildLike(transform(compositeComponents))
|
||||
|
||||
/**
|
||||
* Applies [mapper] to every leaf member of [this] composite type and reassembles the results into composite types
|
||||
* of the same kinds; a non-composite type is passed to [mapper] directly.
|
||||
*
|
||||
* Recurses through nested composites ([PyUnionType], [PyUnsafeUnionType], [PyIntersectionType]) so that [mapper]
|
||||
* always receives a non-composite type, e.g. `Union[Intersection[A, B], C]` is mapped to
|
||||
* `Union[Intersection[f(A), f(B)], f(C)]`. Use [compositeTransform] instead when you need the flat list of the
|
||||
* *direct* members without recursion.
|
||||
*
|
||||
* @see compositeTransform
|
||||
* @see PyUnionType.map
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
fun PyType?.compositeMap(mapper: (PyType?) -> PyType?): PyType? =
|
||||
if (this is PyCompositeType) compositeTransform { members -> members.map { it.compositeMap(mapper) } }
|
||||
else mapper(this)
|
||||
|
||||
/**
|
||||
* Tests [predicate] against the members of [this] composite type with the quantifier that decides whether the
|
||||
* *whole composite is a subtype* of some other type: since `Union[Ts] <: T` iff every `Ti <: T`, *all* members of a
|
||||
* [PyUnionType] must match; since `Intersection[Ts] <: T` (and `UnsafeUnion[Ts] <: T`) iff some `Ti <: T`, *any*
|
||||
* member of a [PyIntersectionType] or a [PyUnsafeUnionType] is enough. For a non-composite type [predicate] is
|
||||
* applied directly.
|
||||
*
|
||||
* [predicate] is expected to test the member on the subtype side, e.g. `compositeMatchesAsSubtype { match(it, target) }`.
|
||||
*
|
||||
* @see compositeMatchesAsSupertype
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
fun PyType?.compositeMatchesAsSubtype(predicate: (PyType?) -> Boolean): Boolean =
|
||||
when (this) {
|
||||
is PyUnionType -> members.all(predicate)
|
||||
is PyIntersectionType, is PyUnsafeUnionType -> members.any(predicate)
|
||||
else -> predicate(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* The mirror of [compositeMatchesAsSubtype]: the quantifier that decides whether some other type is a *subtype of the whole
|
||||
* composite*. Since `T <: Union[Ts]` iff some `T <: Ti`, *any* member of a [PyUnionType] (or a [PyUnsafeUnionType])
|
||||
* suffices; since `T <: Intersection[Ts]` iff `T <: Ti` for every member, *all* members of a [PyIntersectionType]
|
||||
* must match. For a non-composite type [predicate] is applied directly.
|
||||
*
|
||||
* [predicate] is expected to test the member on the supertype side, e.g. `compositeMatchesAsSupertype { match(target, it) }`.
|
||||
*
|
||||
* @see compositeMatchesAsSubtype
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
fun PyType?.compositeMatchesAsSupertype(predicate: (PyType?) -> Boolean): Boolean =
|
||||
when (this) {
|
||||
is PyUnionType, is PyUnsafeUnionType -> members.any(predicate)
|
||||
is PyIntersectionType -> members.all(predicate)
|
||||
else -> predicate(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds a composite type of the same kind as [this] from [members], folding an empty [members] to the identity
|
||||
* element of that kind: `Never` for unions/unsafe unions, `Unknown` for intersections.
|
||||
*/
|
||||
private fun PyType?.rebuildLike(members: List<PyType?>): PyType? =
|
||||
when (this) {
|
||||
is PyIntersectionType -> PyIntersectionType.intersection(members)
|
||||
is PyUnsafeUnionType -> if (members.isEmpty()) PyNeverType.NEVER else PyUnsafeUnionType.unsafeUnion(members)
|
||||
else -> PyUnionType.unionOrNever(members)
|
||||
}
|
||||
|
||||
private fun toUnion(unionFactory: (List<PyType?>) -> PyType?): Collector<PyType?, *, PyType?> {
|
||||
return Collectors.collectingAndThen(Collectors.toList(), unionFactory)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ public class PyUnionType extends PyCompositeTypeBase {
|
||||
* @param members a collection of types to union
|
||||
* @return a PyType representing the union, or null if no valid members
|
||||
*/
|
||||
// TODO: change the default to Never
|
||||
public static @Nullable PyType union(@NotNull Collection<@Nullable PyType> members) {
|
||||
return unionOrDefault(members, PyAnyType.getUnknown());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.types
|
||||
|
||||
import com.intellij.idea.TestFor
|
||||
import com.intellij.openapi.application.runReadActionBlocking
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.allure.Components
|
||||
import com.jetbrains.python.allure.Layers
|
||||
import com.jetbrains.python.allure.Subsystems
|
||||
import com.jetbrains.python.documentation.PythonDocumentationProvider
|
||||
import com.jetbrains.python.fixtures.PyCodeInsightTestCase
|
||||
import com.jetbrains.python.psi.AccessDirection
|
||||
import com.jetbrains.python.psi.PyExpression
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache
|
||||
import com.jetbrains.python.psi.resolve.PyResolveContext
|
||||
import com.jetbrains.python.psi.types.PyCloningTypeVisitor
|
||||
import com.jetbrains.python.psi.types.PyTopType
|
||||
import com.jetbrains.python.psi.types.PyTypeChecker
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@Layers.Functional
|
||||
@TestFor(classes = [PyTopType::class], issues = ["PY-90942"])
|
||||
class PyTopTypeTest : PyCodeInsightTestCase() {
|
||||
@Test
|
||||
fun `matches like builtins object in the type checker`() {
|
||||
myFixture.configureByText("aaa.py", "")
|
||||
runReadActionBlocking {
|
||||
val cache = PyBuiltinCache.getInstance(myFixture.file)
|
||||
val obj = cache.objectType!!
|
||||
val int = cache.intType!!
|
||||
val ctx = TypeEvalContext.codeAnalysis(myFixture.project, myFixture.file)
|
||||
|
||||
// Everything is assignable to the top type, just like `object`.
|
||||
assertTrue(PyTypeChecker.match(PyTopType, int, ctx))
|
||||
assertTrue(PyTypeChecker.match(PyTopType, obj, ctx))
|
||||
assertTrue(PyTypeChecker.match(PyTopType, PyTopType, ctx))
|
||||
// The top type is accepted where `object` is expected.
|
||||
assertTrue(PyTypeChecker.match(obj, PyTopType, ctx))
|
||||
// ...but is not assignable to a narrower type.
|
||||
assertFalse(PyTypeChecker.match(int, PyTopType, ctx))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolves builtins object members`() {
|
||||
val file = myFixture.configureByText("aaa.py", "x = 1")
|
||||
runReadActionBlocking {
|
||||
val obj = PyBuiltinCache.getInstance(file).objectType!!
|
||||
val ctx = TypeEvalContext.codeAnalysis(myFixture.project, file)
|
||||
val resolveContext = PyResolveContext.defaultContext(ctx)
|
||||
val anchor = PsiTreeUtil.findChildOfType(file, PyExpression::class.java)!!
|
||||
|
||||
// A member defined on `object` resolves through the top type, same as through `object` itself.
|
||||
val resolved = PyTopType.resolveMember("__class__", anchor, AccessDirection.READ, resolveContext)
|
||||
assertFalse(resolved.isEmpty())
|
||||
assertEquals(
|
||||
obj.resolveMember("__class__", anchor, AccessDirection.READ, resolveContext)!!.map { it.element },
|
||||
resolved.map { it.element },
|
||||
)
|
||||
|
||||
// A name that `object` does not define resolves to nothing.
|
||||
assertTrue(PyTopType.resolveMember("no_such_member", anchor, AccessDirection.READ, resolveContext).isEmpty())
|
||||
|
||||
// `findMember` / `getAllMembers` mirror `object` too (anchor taken from the context's origin file).
|
||||
assertEquals(obj.findMember("__class__", resolveContext), PyTopType.findMember("__class__", resolveContext))
|
||||
assertEquals(obj.getAllMembers(resolveContext), PyTopType.getAllMembers(resolveContext))
|
||||
assertFalse(PyTopType.getAllMembers(resolveContext).isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `renders like builtins object`() {
|
||||
myFixture.configureByText("aaa.py", "")
|
||||
runReadActionBlocking {
|
||||
val ctx = TypeEvalContext.codeAnalysis(myFixture.project, myFixture.file)
|
||||
val obj = PyBuiltinCache.getInstance(myFixture.file).objectType!!
|
||||
|
||||
assertEquals("object", PythonDocumentationProvider.getTypeName(PyTopType, ctx))
|
||||
assertEquals(PythonDocumentationProvider.getTypeName(obj, ctx), PythonDocumentationProvider.getTypeName(PyTopType, ctx))
|
||||
|
||||
// Under fully qualified rendering the top type is spelled out like the class it stands for.
|
||||
assertEquals("builtins.object", PythonDocumentationProvider.getFullyQualifiedTypeHint(PyTopType, ctx))
|
||||
assertEquals(
|
||||
PythonDocumentationProvider.getFullyQualifiedTypeHint(obj, ctx),
|
||||
PythonDocumentationProvider.getFullyQualifiedTypeHint(PyTopType, ctx),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cloning returns the same singleton`() {
|
||||
myFixture.configureByText("aaa.py", "")
|
||||
runReadActionBlocking {
|
||||
val ctx = TypeEvalContext.codeAnalysis(myFixture.project, myFixture.file)
|
||||
val cloner = object : PyCloningTypeVisitor(ctx) {}
|
||||
assertSame(PyTopType, PyCloningTypeVisitor.clone(PyTopType, cloner))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user