[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
[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_callableemits a TSPFunctionTypethat has areturn_typebut no parameter types (specialized_types: None) and a synthesized declaration with an emptyuri. 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 anytyping.Callable[...]asUnknown/Any. This affectsCallable[...]used as a parameter annotation or a return type.Repro
Hovering
hover TSP renders (via Pylance):Expected (Pyright built-in):
A precise decorator callable such as click's
make_pass_decoratorreturn type collapses the same way (toAny/Unknown).Root cause
pyrefly/lib/tsp/type_conversion.rs,convert_callable:Contrast with
convert_function, which callsself.specialized_types(callable, &ret)to buildSpecializedFunctionTypes { parameter_types, return_type }fromcallable.params. The in-code comment onspecialized_typesexplains why this matters:For a
typing.Callable, there is no source declaration (the synthesized declaration has an emptyuri), so the consumer cannot even derive parameter names — and withspecialized_types: Noneit receives no parameter types either.Suggested fix
Populate
specialized_typesinconvert_callablefrom the callable's parameters (the sameCallableshapespecialized_types()already handles), so the consumer receives the parameter types fortyping.Callable[...]. (Note: consumers that rely on a source declaration for parameter names will additionally need to render a synthesized callable fromspecialized_typesalone, since aCallablehas no backing source node.)Environment
1.2.0-dev.1(built frommain; also reported by users on1.1.1)