From 0fa9446b662a60b624f58fe65baf40904d6b1692 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Sun, 5 Apr 2026 23:37:44 +0900 Subject: [PATCH 1/2] fix --- pyrefly/lib/state/lsp.rs | 61 +++++++++++++++++++++++++++++- pyrefly/lib/test/lsp/hover_type.rs | 32 ++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 2859671591..111749b6eb 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -69,6 +69,8 @@ use crate::alt::answers::Index; use crate::alt::answers_solver::AnswersSolver; use crate::alt::attr::AttrDefinition; use crate::alt::attr::AttrInfo; +use crate::alt::attr::ClassBase; +use crate::alt::class::class_field::ClassAttribute; use crate::binding::binding::Key; use crate::config::error_kind::ErrorKind; use crate::export::exports::Export; @@ -582,6 +584,58 @@ impl<'a> Transaction<'a> { ans.get_type_trace(range) } + fn class_attribute_type_at_definition( + &self, + handle: &Handle, + identifier: &Identifier, + ) -> Option { + let ast = self.get_ast(handle)?; + let mut enclosing_class = None; + for node in Ast::locate_node(&ast, identifier.range.start()) + .into_iter() + .skip(1) + { + match node { + AnyNodeRef::StmtFunctionDef(_) | AnyNodeRef::ExprLambda(_) => return None, + AnyNodeRef::StmtClassDef(class_def) => { + enclosing_class = Some(class_def.name.clone()); + break; + } + _ => {} + } + } + let enclosing_class = enclosing_class?; + let class_key = Key::Definition(ShortIdentifier::new(&enclosing_class)); + let bindings = self.get_bindings(handle)?; + if !bindings.is_valid_key(&class_key) { + return None; + } + let class_ty = self.get_type(handle, &class_key)?; + let attr_name = identifier.id.clone(); + self.ad_hoc_solve( + handle, + "lsp_class_attribute_type_at_definition", + move |solver| { + let class_base = match class_ty { + Type::ClassDef(ref cls) => { + Some(ClassBase::ClassDef(solver.as_class_type_unchecked(cls))) + } + Type::Type(box Type::ClassType(ref cls)) => { + Some(ClassBase::ClassType(cls.clone())) + } + _ => None, + }?; + match solver.get_class_attribute(&class_base, &attr_name)? { + ClassAttribute::ReadWrite(ty) + | ClassAttribute::ReadOnly(ty, _) + | ClassAttribute::Property(ty, _, _) => Some(ty), + ClassAttribute::NoAccess(_) | ClassAttribute::Descriptor(..) => None, + } + }, + ) + .flatten() + } + fn get_chosen_overload_trace(&self, handle: &Handle, range: TextRange) -> Option { let ans = self.get_answers(handle)?; ans.get_chosen_overload_trace(range) @@ -889,7 +943,12 @@ impl<'a> Transaction<'a> { if !bindings.is_valid_key(&key) { return None; } - let mut ty = self.get_type(handle, &key)?; + let mut ty = if matches!(expr_context, ExprContext::Store) { + self.class_attribute_type_at_definition(handle, &id) + .or_else(|| self.get_type(handle, &key))? + } else { + self.get_type(handle, &key)? + }; let call_args_range = self.callee_at(handle, position).and_then( |ExprCall { func, arguments, .. diff --git a/pyrefly/lib/test/lsp/hover_type.rs b/pyrefly/lib/test/lsp/hover_type.rs index 3ea31b59c7..9b578e9aab 100644 --- a/pyrefly/lib/test/lsp/hover_type.rs +++ b/pyrefly/lib/test/lsp/hover_type.rs @@ -225,6 +225,38 @@ Hover Result: `Literal[2]` ); } +#[test] +fn inherited_classvar_hover_prefers_resolved_field_type() { + let code = r#" +from typing import ClassVar + +class A: + x: ClassVar[tuple[str, ...]] = ("A",) + +class B(A): + x = ("B",) +# ^ + +reveal = B.x +# ^ +"#; + let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report); + assert_eq!( + r#" +# main.py +8 | x = ("B",) + ^ +Hover Result: `tuple[str, ...]` + +11 | reveal = B.x + ^ +Hover Result: `tuple[str, ...]` +"# + .trim(), + report.trim(), + ); +} + #[test] fn import_test() { let code = r#" From d3a1d2d085807e0b5fbd5013c7568c2dd55056a1 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 6 Apr 2026 00:12:09 +0900 Subject: [PATCH 2/2] fix --- pyrefly/lib/state/lsp.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 111749b6eb..fec650d56f 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -625,6 +625,11 @@ impl<'a> Transaction<'a> { } _ => None, }?; + let field = solver + .get_field_from_current_class_only(class_base.class_object(), &attr_name)?; + if !field.has_explicit_annotation() { + return None; + } match solver.get_class_attribute(&class_base, &attr_name)? { ClassAttribute::ReadWrite(ty) | ClassAttribute::ReadOnly(ty, _)