From 6415441262c30b4cdf595df54320382d3513c7c8 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 6 Jul 2026 00:32:20 +0900 Subject: [PATCH 1/2] fix --- pyrefly/lib/state/lsp.rs | 93 ++++++++++++++++++++++------------- pyrefly/lib/test/lsp/hover.rs | 19 +++++++ 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 8dd776cc7b..523d201397 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -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 { + 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(¶ms, &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, @@ -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, .. }, @@ -1307,18 +1344,8 @@ impl<'a> Transaction<'a> { pub fn get_expected_type_at(&self, handle: &Handle, position: TextSize) -> Option { // 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(¶ms, &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)?; diff --git a/pyrefly/lib/test/lsp/hover.rs b/pyrefly/lib/test/lsp/hover.rs index bbab562542..5c47996410 100644 --- a/pyrefly/lib/test/lsp/hover.rs +++ b/pyrefly/lib/test/lsp/hover.rs @@ -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#" From c241274c47781c97058b2239fc8c2b62ee5ceec3 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 6 Jul 2026 00:57:41 +0900 Subject: [PATCH 2/2] fix one todo --- pyrefly/lib/test/lsp/hover_type.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrefly/lib/test/lsp/hover_type.rs b/pyrefly/lib/test/lsp/hover_type.rs index 3ea31b59c7..d255a2f0b1 100644 --- a/pyrefly/lib/test/lsp/hover_type.rs +++ b/pyrefly/lib/test/lsp/hover_type.rs @@ -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#" @@ -592,7 +592,7 @@ foo(y="hello") # main.py 11 | foo(x=42) ^ -Hover Result: None +Hover Result: `int` 13 | foo(y="hello") ^