Skip to content

TSP: typing.Callable encoded without parameters (renders as Unknown/Any on hover) #4069

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

convert_callable emits a TSP FunctionType that has a return_type but no parameter types (specialized_types: None) and a synthesized declaration with an empty uri. Because the consumer has neither the converted parameter types nor a source declaration to derive parameter names from, it cannot reconstruct a usable signature and renders any typing.Callable[...] as Unknown/Any. This affects Callable[...] used as a parameter annotation or a return type.

Repro

from typing import Callable

def h(a: int) -> Callable[[int], str]: ...

Hovering h over TSP renders (via Pylance):

(function) def h(a: int) -> Unknown

Expected (Pyright built-in):

(function) def h(a: int) -> ((int) -> str)

A precise decorator callable such as click's make_pass_decorator return type collapses the same way (to Any/Unknown).

Root cause

pyrefly/lib/tsp/type_conversion.rs, convert_callable:

fn convert_callable(&self, callable: &Callable) -> TspType {
    let ret = self.convert(&callable.ret);
    TspType::Function(TspFunctionType {
        bound_to_type: None,
        declaration: Declaration::Synthesized(SynthesizedDeclaration {
            kind: DeclarationKind::Synthesized,
            uri: String::new(),
        }),
        flags: TypeFlags::CALLABLE,
        id: next_id(),
        kind: TypeKind::Function,
        return_type: Some(Box::new(ret)),
        specialized_types: None, // <-- parameters are dropped
        type_alias_info: None,
    })
}

Contrast with convert_function, which calls self.specialized_types(callable, &ret) to build SpecializedFunctionTypes { parameter_types, return_type } from callable.params. The in-code comment on specialized_types explains why this matters:

Pylance rebuilds a function's parameter names by parsing the source declaration, but it cannot evaluate the parameter/return types of an external type server's file ... so they degrade to Unknown. Sending the already-converted types here lets Pylance overlay real types onto the source-derived parameter list.

For a typing.Callable, there is no source declaration (the synthesized declaration has an empty uri), so the consumer cannot even derive parameter names — and with specialized_types: None it receives no parameter types either.

Suggested fix

Populate specialized_types in convert_callable from the callable's parameters (the same Callable shape specialized_types() already handles), so the consumer receives the parameter types for typing.Callable[...]. (Note: consumers that rely on a source declaration for parameter names will additionally need to render a synthesized callable from specialized_types alone, since a Callable has no backing source node.)

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