Skip to content
Closed
109 changes: 109 additions & 0 deletions crates/pyrefly_types/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ pub struct TypeDisplayContext<'a> {
/// Render a `DataFrame` as its plain underlying class rather than the schema form.
strip_library_schemas: bool,
render_unknown_as_any: bool,
render_source_annotation: bool,
/// Optional stdlib reference for resolving builtin type locations
stdlib: Option<&'a Stdlib>,
/// Stack of identities of type variables currently bound by enclosing Foralls.
Expand Down Expand Up @@ -342,6 +343,7 @@ impl<'a> TypeDisplayContext<'a> {
fn fmt_quantified(&self, quantified: &Quantified, output: &mut impl TypeOutput) -> fmt::Result {
write!(output, "{}", quantified.name)?;
if self.always_display_module_name
&& !self.render_source_annotation
&& !self
.forall_tparam_uniques
.borrow()
Expand Down Expand Up @@ -920,6 +922,60 @@ impl<'a> TypeDisplayContext<'a> {
is_toplevel: bool,
output: &mut impl TypeOutput,
) -> fmt::Result {
if self.render_source_annotation
&& let Some(metadata) = t.toplevel_func_metadata()
&& let FunctionKind::CallbackProtocol(cls) = &metadata.kind
{
output.write_qname(cls.qname())?;
return output.write_targs(cls.targs());
}
if self.render_source_annotation
&& let Some((callable, _)) = t.toplevel_callable_signatures().next()
{
let bound_signature = if matches!(t, Type::BoundMethod(_)) {
callable.strip_first_param()
} else {
None
};
let callable = bound_signature.as_ref().unwrap_or(callable);
self.maybe_fmt_with_module("typing", "Callable", output)?;
output.write_str("[")?;
match &callable.params {
Params::List(params)
if params
.items()
.iter()
.all(|p| matches!(p, Param::PosOnly(..) | Param::Pos(..))) =>
{
output.write_str("[")?;
self.fmt_type_sequence(
params.items().iter().map(|p| p.as_type()),
", ",
false,
output,
)?;
output.write_str("]")?;
}
Params::ParamSpec(prefix, param_spec) => {
if prefix.is_empty() {
self.fmt_helper_generic(param_spec, false, output)?;
} else {
self.maybe_fmt_with_module("typing", "Concatenate", output)?;
output.write_str("[")?;
for param in prefix {
self.fmt_helper_generic(param.ty(), false, output)?;
output.write_str(", ")?;
}
self.fmt_helper_generic(param_spec, false, output)?;
output.write_str("]")?;
}
}
_ => output.write_str("...")?,
}
output.write_str(", ")?;
self.fmt_helper_generic(&callable.ret, false, output)?;
return output.write_str("]");
}
match t {
// Things that have QName's and need qualifying
Type::ClassDef(cls) => {
Expand Down Expand Up @@ -1638,6 +1694,12 @@ impl<'a> TypeDisplayContext<'a> {
Type::Quantified(var) => self.fmt_quantified(var, output),
Type::QuantifiedValue(var) => write!(output, "{var}"),
Type::ElementOfTypeVarTuple(var) => write!(output, "ElementOf[{var}]"),
Type::Args(q) if self.render_source_annotation => {
write!(output, "{}.args", q.name())
}
Type::Kwargs(q) if self.render_source_annotation => {
write!(output, "{}.kwargs", q.name())
}
Type::Args(q) => {
output.write_str("Args[")?;
write!(output, "{q}")?;
Expand Down Expand Up @@ -1857,6 +1919,7 @@ impl Type {
pub fn get_annotation_parts(&self, stdlib: Option<&Stdlib>) -> Vec<AnnotationPart> {
let mut ctx = annotation_context(self, stdlib);
ctx.render_unknown_as_any = true;
ctx.render_source_annotation = true;
ctx.always_display_module_name_except_builtins();
let mut output = AnnotationOutput::new(&ctx);
ctx.fmt_helper_generic(self, false, &mut output).unwrap();
Expand Down Expand Up @@ -3617,6 +3680,52 @@ def overloaded_func[T](
assert!(parts.iter().any(|(_, loc)| loc.is_some()));
}

#[test]
fn source_annotation_renders_nested_callables() {
let inner = Type::Callable(Box::new(Callable::list(
ParamList::new(vec![Param::PosOnly(None, Type::None, Required::Required)]),
Type::any_implicit(),
)));
let ty = Type::Callable(Box::new(Callable::list(
ParamList::new(vec![Param::PosOnly(None, inner, Required::Required)]),
Type::None,
)));
assert_eq!(ty.to_string(), "((None) -> Unknown) -> None");
assert_eq!(
ty.get_annotation_parts(None),
vec![
AnnotationPart::Reference {
module: ModuleName::from_str("typing"),
name: "Callable".to_owned()
},
AnnotationPart::Text("[[".to_owned()),
AnnotationPart::Reference {
module: ModuleName::from_str("typing"),
name: "Callable".to_owned()
},
AnnotationPart::Text("[[None], ".to_owned()),
AnnotationPart::Reference {
module: ModuleName::from_str("typing"),
name: "Any".to_owned()
},
AnnotationPart::Text("]], None]".to_owned()),
]
);
}

#[test]
fn source_annotation_omits_bound_receiver() {
let annotation = fake_bound_method("method", "Class", "module")
.get_annotation_parts(None)
.into_iter()
.map(|part| match part {
AnnotationPart::Text(text) => text,
AnnotationPart::Reference { name, .. } => name,
})
.collect::<String>();
assert_eq!(annotation, "Callable[[Any, Any], None]");
}

#[test]
fn source_annotation_surfaces_render_self_as_self() {
let cls = fake_class("MyClass", "mymodule", 40);
Expand Down
17 changes: 17 additions & 0 deletions pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3691,6 +3691,13 @@ impl<'a> Transaction<'a> {
// Actions such as missing-`@override` and context-manager annotations
// carry multiple edits that must apply together.
let mut multi_actions: Vec<(String, Vec<(Module, TextRange, String)>)> = Vec::new();
if let Some(actions) = self.implement_abstract_members_code_actions(handle, range) {
multi_actions.extend(
actions
.into_iter()
.map(|action| (action.title, action.edits)),
);
}
// Deduplicates the actions pushed onto `other_actions`. The same fix can
// be generated more than once -- e.g. several errors on one line all
// produce an identical "add pyrefly ignore" action, or one unused binding
Expand Down Expand Up @@ -4195,6 +4202,16 @@ impl<'a> Transaction<'a> {
quick_fixes::extract_superclass::extract_superclass_code_actions(self, handle, selection)
}

pub fn implement_abstract_members_code_actions(
&self,
handle: &Handle,
selection: TextRange,
) -> Option<Vec<LocalRefactorCodeAction>> {
quick_fixes::implement_abstract_members::implement_abstract_members_code_actions(
self, handle, selection,
)
}

pub fn pull_members_up_code_actions(
&self,
handle: &Handle,
Expand Down
1 change: 1 addition & 0 deletions pyrefly/lib/state/lsp/quick_fixes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod extract_shared;
pub(crate) mod extract_superclass;
pub(crate) mod extract_variable;
pub(crate) mod generate_code;
pub(crate) mod implement_abstract_members;
pub(crate) mod implicit_import;
pub(crate) mod inline_method;
pub(crate) mod inline_parameter;
Expand Down
Loading
Loading