Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 60 additions & 33 deletions pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,29 @@ impl<'a> Transaction<'a> {
}
}

fn get_active_call_argument_type_for_surface(
&self,
handle: &Handle,
position: TextSize,
for_display: bool,
) -> Option<Type> {
let CallInfo {
callables,
chosen_overload_index,
active_argument,
..
} = self.get_callables_from_call(handle, position)?;
let callable = callables.get(chosen_overload_index.unwrap_or(0)).cloned()?;
let params = Self::normalize_singleton_function_type_into_params(callable)?;
let arg_index = Self::active_parameter_index(&params, &active_argument)?;
let ty = params.get(arg_index)?.as_type().clone();
if for_display {
Some(self.get_answers(handle)?.solver().for_display(ty))
} else {
Some(ty)
}
}

fn import_handle_with_preference(
&self,
handle: &Handle,
Expand Down Expand Up @@ -1157,30 +1180,44 @@ impl<'a> Transaction<'a> {
Some(IdentifierWithContext {
identifier,
context: IdentifierContext::KeywordArgument(callee_kind),
}) => self
.find_definition_for_keyword_argument(
handle,
&identifier,
&callee_kind,
FindPreference::default(),
)
.first()
.and_then(|item| {
}) => {
for item in self
.find_definition_for_keyword_argument(
handle,
&identifier,
&callee_kind,
FindPreference::default(),
)
.into_iter()
{
let code_at_range = item.module.code_at(item.definition_range);
// If refinement failed, definition_range points to the callee itself,
// not a matching parameter. In that case, return None.
if code_at_range != identifier.id.as_str() {
return None;
}
let name = Name::new(code_at_range);
let id = Identifier::new(name.clone(), item.definition_range);
let key = Key::Definition(ShortIdentifier::new(&id));
let bindings = self.get_bindings(handle)?;
if !bindings.is_valid_key(&key) {
return None;
// not a matching parameter, so try another definition or fall back to
// the call signature below.
if code_at_range == identifier.id.as_str() {
let definition_handle = Handle::new(
item.module.name(),
item.module.path().dupe(),
handle.sys_info().dupe(),
);
let id = Identifier::new(Name::new(code_at_range), item.definition_range);
let key = Key::Definition(ShortIdentifier::new(&id));
if self
.get_bindings(&definition_handle)
.is_some_and(|bindings| bindings.is_valid_key(&key))
&& let Some(ty) =
self.get_type_for_surface(&definition_handle, &key, for_display)
{
return Some(ty);
}
}
self.get_type_for_surface(handle, &key, for_display)
}),
}
self.get_active_call_argument_type_for_surface(
handle,
identifier.range.start(),
for_display,
)
}
Some(IdentifierWithContext {
identifier: _,
context: IdentifierContext::Attribute { range, .. },
Expand Down Expand Up @@ -1307,18 +1344,8 @@ impl<'a> Transaction<'a> {
pub fn get_expected_type_at(&self, handle: &Handle, position: TextSize) -> Option<Type> {
// Call-argument position: predict the active parameter's type from the
// call signature. Works for not-yet-typed arguments and selects an overload.
if let Some(CallInfo {
callables,
chosen_overload_index,
active_argument,
..
}) = self.get_callables_from_call(handle, position)
&& let Some(callable) = callables.get(chosen_overload_index.unwrap_or(0)).cloned()
&& let Some(params) = Self::normalize_singleton_function_type_into_params(callable)
&& let Some(arg_index) = Self::active_parameter_index(&params, &active_argument)
&& let Some(param) = params.get(arg_index)
{
return Some(param.as_type().clone());
if let Some(ty) = self.get_active_call_argument_type_for_surface(handle, position, false) {
return Some(ty);
}

let module = self.get_ast(handle)?;
Expand Down
19 changes: 19 additions & 0 deletions pyrefly/lib/test/lsp/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,25 @@ foo(x=1, y=2)
assert!(report.contains("documentation for y"));
}

#[test]
fn hover_shows_type_for_imported_keyword_argument() {
let lib = r#"
def foo(x: int, y: str) -> None: ...
"#;
let code = r#"
from lib import foo

foo(x=1, y="hello")
# ^
"#;
let report =
get_batched_lsp_operations_report(&[("main", code), ("lib", lib)], get_test_report);
assert!(
report.contains("y: str"),
"Expected keyword argument hover to show imported parameter type, got: {report}"
);
}

#[test]
fn hover_returns_none_for_docstring_literals() {
let code = r#"
Expand Down
6 changes: 3 additions & 3 deletions pyrefly/lib/test/lsp/hover_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,8 @@ Hover Result: `int`
);
}

// todo(kylei): When the callee's implementation uses *args/**kwargs, we can't refine the
// keyword argument to a specific parameter. Ideally we'd resolve through the matched overload.
// When the callee's implementation uses *args/**kwargs, keyword argument hovers fall back
// to the matched overload when there is one.
#[test]
fn kwarg_with_overload() {
let code = r#"
Expand All @@ -592,7 +592,7 @@ foo(y="hello")
# main.py
11 | foo(x=42)
^
Hover Result: None
Hover Result: `int`

13 | foo(y="hello")
^
Expand Down
Loading