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
11 changes: 8 additions & 3 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,10 +698,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
};
let hint = None; // discard hint
let class_metadata = self.get_metadata_for_class(cls.class_object());
let metaclass_dunder_call = self.get_metaclass_dunder_call(&cls);
if let Some(ret) =
self.call_metaclass(&cls, arguments_range, args, keywords, errors, context, hint)
Comment on lines 699 to 703
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

construct_class now calls get_metaclass_dunder_call unconditionally (and that internally calls get_metadata_for_class), even when the class has no custom metaclass __call__. Since constructor analysis is on a hot path, this adds an extra metadata/member lookup for every class construction. Consider deriving a has_metaclass_call bool from call_metaclass(...) (or returning the resolved __call__ type from it) so you only perform the metaclass lookup when it actually exists, or refactor get_metaclass_dunder_call to reuse the already-computed class_metadata.

Copilot uses AI. Check for mistakes.
{
if let Some(metaclass_dunder_call) = self.get_metaclass_dunder_call(&cls) {
if let Some(metaclass_dunder_call) = metaclass_dunder_call.clone() {
if let Some(callee_range) = callee_range
Comment on lines 704 to 706
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The outer metaclass_dunder_call is an Option<...> but the inner if let Some(metaclass_dunder_call) = metaclass_dunder_call.clone() reuses the same identifier for the unwrapped value, which is easy to misread (especially since the outer value is still used later for gating trace recording). Renaming the inner binding (e.g. metaclass_dunder_call_ty) would make the trace-priority logic clearer.

Copilot uses AI. Check for mistakes.
&& let Some(metaclass) = class_metadata.custom_metaclass()
{
Expand Down Expand Up @@ -769,7 +770,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
callee_range,
);
}
self.record_resolved_trace(arguments_range, new_method);
if metaclass_dunder_call.is_none() {
self.record_resolved_trace(arguments_range, new_method);
}
if self.is_compatible_constructor_return(&ret, cls.class_object()) {
dunder_new_ret = Some(ret);
} else if !matches!(ret, Type::Any(AnyStyle::Error | AnyStyle::Implicit)) {
Expand Down Expand Up @@ -824,7 +827,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
callee_range,
);
}
self.record_resolved_trace(arguments_range, init_method);
if !overrides_new && metaclass_dunder_call.is_none() {
self.record_resolved_trace(arguments_range, init_method);
}
}
if class_metadata.is_pydantic_model()
&& let Some(dataclass) = class_metadata.dataclass_metadata()
Expand Down
23 changes: 23 additions & 0 deletions pyrefly/lib/test/lsp/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,29 @@ Person("Alice", 25)
);
}

#[test]
fn hover_on_namedtuple_constructor_shows_field_signature() {
let code = r#"
from typing import NamedTuple

class Test(NamedTuple):
a: str
b: int

Test()
#^
"#;
let report = get_batched_lsp_operations_report_allow_error(&[("main", code)], get_test_report);
assert!(
report.contains("a: str") && report.contains("b: int") && report.contains("-> Test"),
"Expected NamedTuple constructor hover to show field parameters, got: {report}"
);
assert!(
!report.contains("*Unknown") && !report.contains("**Unknown"),
"NamedTuple constructor hover should not fall back to variadic Unknown params, got: {report}"
);
}

#[test]
fn hover_over_in_keyword_in_for_loop() {
let code = r#"
Expand Down
25 changes: 25 additions & 0 deletions pyrefly/lib/test/lsp/signature_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,31 @@ Signature Help Result: active=0
);
}

#[test]
fn namedtuple_constructor_signature_shows_namedtuple_fields() {
let code = r#"
from typing import NamedTuple

class Test(NamedTuple):
a: str
b: int

Test()
# ^
Test(a="", )
# ^
"#;
let report = get_batched_lsp_operations_report_allow_error(&[("main", code)], get_test_report);
assert!(
report.contains("a: str") && report.contains("b: int") && report.contains("-> Test"),
"Expected NamedTuple signature help to show field parameters, got: {report}"
);
assert!(
!report.contains("*Unknown") && !report.contains("**Unknown"),
"NamedTuple signature help should not fall back to variadic Unknown params, got: {report}"
);
}

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