Skip to content

TSP: type[T] over a TypeVar loses the type[...] wrapper (rendered as bare T) #4068

Description

@rchiodo

[TSP] Found via differential testing comparing Pyright against Pyrefly running as an external type server over the Type Server Protocol (TSP). Surfaced downstream in Pylance: microsoft/pylance-release#8091

Summary

When converting a type[X] type to TSP, Pyrefly only marks the result INSTANTIABLE when the inner type is a class. For type[T] where T is a TypeVar (or any other non-class inner type), the type[...] wrapper is silently dropped: the TSP consumer receives a bare TypeVar, indistinguishable from a plain T.

Repro

from typing import TypeVar

T = TypeVar("T")

def f(object_type: type[T]) -> type[T]: ...

Hovering f over TSP renders (via Pylance):

(function) def f(object_type: T@f) -> T@f

Expected (Pyright built-in):

(function) def f(object_type: type[T@f]) -> type[T@f]

type[int] (a concrete class) renders correctly as type[int]; only type[TypeVar] (and similar non-class inner types, e.g. type[Self] for a user-defined Self) is affected.

Root cause

pyrefly/lib/tsp/type_conversion.rs, the PyreflyType::Type(inner) arm:

// --- type[X] wrapper ---
PyreflyType::Type(inner) => {
    let inner_tsp = self.convert(inner);
    // Return the inner type but mark it as instantiable
    match inner_tsp {
        TspType::Class(mut c) => {
            c.flags = TypeFlags::INSTANTIABLE;
            TspType::Class(c)
        }
        other => other, // <-- non-Class inner (e.g. a TypeVar) loses the type[...] wrapper
    }
}

For a TspType::Var (and any non-Class variant), the other => other branch returns the inner type unchanged. Its flags were set to TypeFlags::NONE by make_typevar_declared (which hardcodes flags: TypeFlags::NONE), so the emitted type[T] is byte-for-byte identical to a plain T. The consumer has no signal to reconstruct the type[...] wrapper.

The TSP protocol supports this: TypeVarType extends the base Type and therefore carries a flags field, and TypeFlags::INSTANTIABLE (1 << 0) is a valid flag for it.

Suggested fix

In the other => other branch, set TypeFlags::INSTANTIABLE on the converted inner type's flags (as is already done for the TspType::Class case) so consumers can distinguish type[T] from T and reconstruct the wrapper.

Environment

  • Pyrefly: 1.2.0-dev.1 (built from main; also reported by users on 1.1.1)
  • OS: Windows

Metadata

Metadata

Assignees

Labels

language-serverIssues specific to our IDE integration rather than type checking

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions