Summary
When Pyrefly serves a type over TSP for a definition-site request — a function def, a method def, or an @property getter — the function's inferred return type is omitted: the wire Function carries return_type = Unknown. The same function requested at a reference / call site correctly carries its inferred return type. As a result, hovering an unannotated @property renders ... -> Unknown (originally reported as Any on 1.1.1) instead of the inferred type.
No protocol change is required. The existing Function.return_type field already conveys inferred returns — reference sites prove it (see the table below). Only the definition-site conversion path fails to populate it.
User-visible effect
Reported downstream as microsoft/pylance-release#8096. Hovering an unannotated @property getter (e.g. requests.Response.ok, apparent_encoding):
- Expected (Pylance built-in Pyright):
(property) ok: (self: Self@Response) -> bool
- Actual (Pyrefly over TSP):
(property) ok: (self: Self@Response) -> Unknown
Evidence (Pyrefly 1.2.0-dev.1 as the TSP server)
Hovering these constructs and observing the type Pyrefly sends over TSP:
| Hover target |
Pyrefly TSP output |
plain f reference — def f(): return True then a bare f |
(function) def f() -> Literal[True] ✅ |
plain def f definition |
(function) f: Unknown ❌ |
def m(self) method definition |
(method) def m(self: Self@C) -> Unknown ❌ |
@property def ok(self) getter |
(property) ok: (self: Self@C) -> Unknown ❌ |
Pyrefly's own checker infers all of these correctly, so the type is available and is lost only in the definition-site TSP conversion:
reveal_type(c.ok) # revealed type: Literal[True]
reveal_type(c.apparent_encoding) # revealed type: Literal['utf-8']
Why this confirms no protocol change is needed
- The protocol already models function return types:
crates/tsp_types/src/protocol.rs — FunctionType.return_type: Option<Box<Type>>.
- Pyrefly already populates it:
pyrefly/lib/tsp/type_conversion.rs — convert_function(...) sets return_type: Some(Box::new(ret)).
- The reference-site row above shows this working end-to-end (
-> Literal[True]), using the current protocol only.
The gap is specifically the definition-site path. In pyrefly/lib/state/lsp.rs, get_type_at_impl_with_options resolves a use-site via Key::BoundName (the fully-solved function whose .ret is inferred), but resolves a definition-site (FunctionDef / MethodDef identifier contexts) via Key::Definition → get_type_for_surface → get_type_at(idx), which yields the function type before return-type inference is reflected. The @property getter surfaces the same definition-site behavior, which is what the downstream hover renders.
Suggested fix
When resolving a definition-site type for a function / method / property-getter, produce the same solved type the reference-site path yields (i.e. ensure the inferred return type is computed and attached) before converting to the TSP Function. No changes to tsp_types or the wire protocol are needed.
Context
Summary
When Pyrefly serves a type over TSP for a definition-site request — a function
def, a methoddef, or an@propertygetter — the function's inferred return type is omitted: the wireFunctioncarriesreturn_type = Unknown. The same function requested at a reference / call site correctly carries its inferred return type. As a result, hovering an unannotated@propertyrenders... -> Unknown(originally reported asAnyon1.1.1) instead of the inferred type.No protocol change is required. The existing
Function.return_typefield already conveys inferred returns — reference sites prove it (see the table below). Only the definition-site conversion path fails to populate it.User-visible effect
Reported downstream as microsoft/pylance-release#8096. Hovering an unannotated
@propertygetter (e.g.requests.Response.ok,apparent_encoding):(property) ok: (self: Self@Response) -> bool(property) ok: (self: Self@Response) -> UnknownEvidence (Pyrefly
1.2.0-dev.1as the TSP server)Hovering these constructs and observing the type Pyrefly sends over TSP:
freference —def f(): return Truethen a baref(function) def f() -> Literal[True]✅def fdefinition(function) f: Unknown❌def m(self)method definition(method) def m(self: Self@C) -> Unknown❌@property def ok(self)getter(property) ok: (self: Self@C) -> Unknown❌Pyrefly's own checker infers all of these correctly, so the type is available and is lost only in the definition-site TSP conversion:
Why this confirms no protocol change is needed
crates/tsp_types/src/protocol.rs—FunctionType.return_type: Option<Box<Type>>.pyrefly/lib/tsp/type_conversion.rs—convert_function(...)setsreturn_type: Some(Box::new(ret)).-> Literal[True]), using the current protocol only.The gap is specifically the definition-site path. In
pyrefly/lib/state/lsp.rs,get_type_at_impl_with_optionsresolves a use-site viaKey::BoundName(the fully-solved function whose.retis inferred), but resolves a definition-site (FunctionDef/MethodDefidentifier contexts) viaKey::Definition→get_type_for_surface→get_type_at(idx), which yields the function type before return-type inference is reflected. The@propertygetter surfaces the same definition-site behavior, which is what the downstream hover renders.Suggested fix
When resolving a definition-site type for a function / method / property-getter, produce the same solved type the reference-site path yields (i.e. ensure the inferred return type is computed and attached) before converting to the TSP
Function. No changes totsp_typesor the wire protocol are needed.Context
Noneencoded as BuiltInType name:"none", violating the documented BuiltInType.name contract #4035 (Noneencoding).main(dev build1.2.0-dev.1).