From c0937eb9931783ec2219e5da9d6404dbd5641b49 Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 27 Oct 2026 15:42:55 -0300 Subject: [PATCH] Infer completions from __getattr__ literals --- pyrefly/lib/alt/attr.rs | 43 +++++++++++++++++++++++++++++- pyrefly/lib/test/lsp/completion.rs | 17 ++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 727fa5a059..4ec20aaad0 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -3638,7 +3638,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { ) -> Vec { let annotated_fields = self.django_annotated_fields(&base); let mut res = Vec::new(); - if let Some(base) = self.as_attribute_base(base) { + if let Some(base) = self.as_attribute_base(base.clone()) { self.completions_inner( base, expected_attribute_name, @@ -3647,6 +3647,47 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { &mut res, ); } + if expected_attribute_name.is_none() + && let Some(getattr) = self.type_of_magic_dunder_attr( + &base, + &dunder::GETATTR, + TextRange::default(), + &self.error_swallower(), + None, + "completion_getattr", + false, + ) + { + let param_index = usize::from(matches!(&getattr, Type::BoundMethod(_))); + for (callable, _) in getattr.toplevel_callable_signatures() { + let Some(param) = callable.get_positional_param(param_index) else { + continue; + }; + let members = match param { + Type::Union(union) => union.members.as_slice(), + _ => std::slice::from_ref(param), + }; + for member in members { + let Type::Literal(lit) = member else { + continue; + }; + let Lit::Str(value) = &lit.value else { + continue; + }; + let name = Name::new(value); + if !res.iter().any(|info| info.name == name) { + res.push(AttrInfo { + name, + ty: include_types.then(|| callable.ret.clone()), + is_deprecated: false, + definition: AttrDefinition::Synthetic, + is_reexport: false, + is_from_object: false, + }); + } + } + } + } for (name, ty) in annotated_fields { if expected_attribute_name.is_none_or(|expected| expected == &name) && !res.iter().any(|info| info.name == name) diff --git a/pyrefly/lib/test/lsp/completion.rs b/pyrefly/lib/test/lsp/completion.rs index ca6dff7de1..35fe0accf7 100644 --- a/pyrefly/lib/test/lsp/completion.rs +++ b/pyrefly/lib/test/lsp/completion.rs @@ -382,6 +382,23 @@ Completion Results: ); } +#[test] +fn dot_complete_from_getattr_literal_parameter() { + let code = r#" +from typing import Literal + +class A: + def __getattr__(self, item: Literal["A", "B"]) -> object: ... + +A(). +# ^ +"#; + let report = + get_batched_lsp_operations_report_allow_error(&[("main", code)], get_default_test_report()); + assert!(report.contains("- (Field) A"), "{report}"); + assert!(report.contains("- (Field) B"), "{report}"); +} + #[test] fn dict_key_completion_from_literal() { let code = r#"