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
66 changes: 65 additions & 1 deletion pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -582,6 +584,63 @@ impl<'a> Transaction<'a> {
ans.get_type_trace(range)
}

fn class_attribute_type_at_definition(
&self,
handle: &Handle,
identifier: &Identifier,
) -> Option<Type> {
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;
}
_ => {}
}
Comment on lines +594 to +605
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class_attribute_type_at_definition only treats function defs and lambdas as scope boundaries. Comprehensions/generator expressions also introduce their own scope, so a Store-context name inside a comprehension in a class body could be misinterpreted as a class attribute and incorrectly resolve to an inherited field type. Consider bailing out when Ast::locate_node includes AnyNodeRef::ExprListComp|ExprSetComp|ExprDictComp|ExprGenerator (and/or AnyNodeRef::Comprehension) similar to how other code treats these as disallowed scopes.

Copilot uses AI. Check for mistakes.
}
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,
}?;
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, _)
| ClassAttribute::Property(ty, _, _) => Some(ty),
ClassAttribute::NoAccess(_) | ClassAttribute::Descriptor(..) => None,
}
},
)
.flatten()
}

fn get_chosen_overload_trace(&self, handle: &Handle, range: TextRange) -> Option<Type> {
let ans = self.get_answers(handle)?;
ans.get_chosen_overload_trace(range)
Expand Down Expand Up @@ -889,7 +948,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)?
};
Comment on lines +951 to +956
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change only affects Transaction::get_type_at (hover/provide-type). Inlay hints are generated via Transaction::inlay_hints and currently call get_type(handle, key) for Key::Definition, so the original Issue #846 symptom may still reproduce for variable-type inlay hints. If the goal is to fix inlay hints, the same “prefer resolved class field type” logic likely needs to be applied in the inlay hint path as well.

Copilot uses AI. Check for mistakes.
let call_args_range = self.callee_at(handle, position).and_then(
|ExprCall {
func, arguments, ..
Expand Down
32 changes: 32 additions & 0 deletions pyrefly/lib/test/lsp/hover_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
}
Comment on lines +228 to +258
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test exercises hover behavior, but Issue #846 is about variable-type inlay hints showing an overly specific inferred type on inherited attributes. Since the inlay hint pipeline doesn’t use hover’s get_type_at path, it would be good to add/adjust a test in the inlay hint test suite to ensure the reported inlay hint output is fixed (and to prevent regressions).

Copilot uses AI. Check for mistakes.

#[test]
fn import_test() {
let code = r#"
Expand Down
Loading