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
28 changes: 21 additions & 7 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2657,7 +2657,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
if ty.is_some_and(Self::is_proxy_method_type) {
return true;
}
let BindingAnnotation::AnnotateExpr(_, expr, _) = self.bindings().get(annotation) else {
let BindingAnnotation::AnnotateExpr(_, expr, _, _) = self.bindings().get(annotation) else {
return false;
};
Self::proxy_method_annotation_syntax_mentions_name(expr)
Expand All @@ -2669,7 +2669,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
&self,
annotation: Idx<KeyAnnotation>,
) -> ProxyMethodAnnotationForm {
let BindingAnnotation::AnnotateExpr(_, expr, _) = self.bindings().get(annotation) else {
let BindingAnnotation::AnnotateExpr(_, expr, _, _) = self.bindings().get(annotation) else {
return ProxyMethodAnnotationForm::Other;
};
self.proxy_method_annotation_expr_form(expr)
Expand Down Expand Up @@ -4885,12 +4885,14 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
})
}

/// Return the first inherited method signature (parameters and flags) for `name`.
pub(crate) fn inherited_method_signature(
/// Return the instantiated type of the first inherited member for `name`
/// whose type is accepted by `accept`, walking the MRO.
pub(crate) fn inherited_member_where(
&self,
cls: &Class,
name: &Name,
) -> Option<(ParamList, FuncFlags)> {
accept: impl Fn(&Type) -> bool,
) -> Option<Type> {
let derived_instance = self.instantiate(cls);
for ancestor in self.get_mro_for_class(cls).ancestors(self.stdlib) {
let parent_cls = ancestor.class_object();
Expand All @@ -4899,13 +4901,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
};
let instance = Instance::of_protocol(ancestor, derived_instance.clone());
let instantiated = member.instantiate_for(self.heap, &instance);
if let Some(sig) = Self::callable_params_and_flags(instantiated.ty()) {
return Some(sig);
let ty = instantiated.ty();
if accept(&ty) {
return Some(ty);
}
}
None
}

/// Return the first inherited method signature (parameters and flags) for `name`.
pub(crate) fn inherited_method_signature(
&self,
cls: &Class,
name: &Name,
) -> Option<(ParamList, FuncFlags)> {
Self::callable_params_and_flags(self.inherited_member_where(cls, name, |ty| {
Self::callable_params_and_flags(ty.clone()).is_some()
})?)
}

pub fn get_metaclass_attribute(
&self,
cls: &ClassBase,
Expand Down
4 changes: 3 additions & 1 deletion pyrefly/lib/alt/class/pydantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
if !metadata.is_pydantic_model() {
return None;
}
if let BindingAnnotation::AnnotateExpr(_, annotation_expr, _) = self.bindings().get(annot) {
if let BindingAnnotation::AnnotateExpr(_, annotation_expr, _, _) =
self.bindings().get(annot)
{
let mut keywords = None;
let metadata_items = self.get_annotated_metadata(
annotation_expr,
Expand Down
95 changes: 92 additions & 3 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ use crate::types::annotation::Qualifier;
use crate::types::callable::Callable;
use crate::types::callable::Param;
use crate::types::callable::ParamList;
use crate::types::callable::Params;
use crate::types::callable::Required;
use crate::types::class::AttrsFieldSpecifierKind;
use crate::types::class::Class;
Expand Down Expand Up @@ -545,9 +546,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors: &ErrorCollector,
) -> AnnotationWithTarget {
match binding {
BindingAnnotation::AnnotateExpr(target, x, class_key) => {
BindingAnnotation::AnnotateExpr(target, x, class_key, override_method) => {
let type_form_context = target.type_form_context();
let mut ann = self.expr_annotation(x, type_form_context, errors);
// An `@override` method repeating the base signature's `Any` is exempt
// from `explicit-any`; solve into a local collector only then, so the
// dictated error can be filtered out.
let may_exempt = override_method.is_some()
&& matches!(
type_form_context,
TypeFormContext::ParameterAnnotation
| TypeFormContext::ParameterArgsAnnotation
| TypeFormContext::ParameterKwargsAnnotation
| TypeFormContext::ReturnAnnotation
);
let ann_errors = may_exempt.then(|| self.error_collector());
let mut ann = self.expr_annotation(
x,
type_form_context,
ann_errors.as_ref().unwrap_or(errors),
);
if let Some(class_key) = class_key
&& let Some(ty) = &mut ann.ty
{
Expand Down Expand Up @@ -580,6 +597,18 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
);
}
}
if let Some(ann_errors) = ann_errors {
// An `@override` method cannot narrow or drop an `Any` the base
// signature declares: repeating it is required, so the
// `explicit-any` reported where it is written is dropped.
let dictated = matches!(&ann.ty, Some(Type::Any(AnyStyle::Explicit)))
&& override_method.as_ref().is_some_and(|method| {
self.override_base_dictates_any(*class_key, method, target)
});
errors.extend_filtered(ann_errors, |error| {
!(dictated && error.error_kind() == ErrorKind::ExplicitAny)
});
}
AnnotationWithTarget {
target: target.clone(),
annotation: ann,
Expand Down Expand Up @@ -3764,7 +3793,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
/// Returns `None` for special forms which don't have a source expression.
pub(crate) fn annotation_range(&self, key: Idx<KeyAnnotation>) -> Option<TextRange> {
match self.bindings().get(key) {
BindingAnnotation::AnnotateExpr(_, expr, _) => Some(expr.range()),
BindingAnnotation::AnnotateExpr(_, expr, _, _) => Some(expr.range()),
BindingAnnotation::SpecialForm(..) => None,
}
}
Expand Down Expand Up @@ -7082,6 +7111,66 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
ty
}

/// For a parameter or return annotation of an `@override` method: does the
/// base-class signature dictate `Any` at this position? The override cannot
/// narrow or drop such an `Any` without breaking the override, so it is
/// exempt from `explicit-any`. Only an explicitly-`Any`-annotated base
/// position exempts — an unannotated one does not.
fn override_base_dictates_any(
&self,
class_key: Option<Idx<KeyClass>>,
method_name: &Name,
target: &AnnotationTarget,
) -> bool {
let Some(class_key) = class_key else {
return false;
};
let class = &*self.get_idx(class_key);
let Some(cls) = &class.0 else {
return false;
};
let Some(base) =
self.inherited_member_where(cls, method_name, |ty| ty.is_toplevel_callable())
else {
return false;
};
let is_explicit_any = |ty: &Type| matches!(ty, Type::Any(AnyStyle::Explicit));
// An async base stores its declared return as `Coroutine[Any, Any, T]`.
let return_dictates = |ret: &Type| {
is_explicit_any(ret)
|| self
.unwrap_coroutine(ret)
.is_some_and(|(_, _, ret)| is_explicit_any(&ret))
};
base.toplevel_callable_signatures().any(|(sig, _)| {
let items: &[Param] = match &sig.params {
Params::List(params) | Params::Partial(params) => params.items(),
_ => &[],
};
match target {
AnnotationTarget::Return(_) => return_dictates(&sig.ret),
AnnotationTarget::Param(name) => items.iter().any(|p| match p {
Param::Pos(param_name, ty, _) | Param::KwOnly(param_name, ty, _) => {
param_name.as_str() == name.as_str()
&& matches!(ty, Type::Any(AnyStyle::Explicit))
}
Param::PosOnly(Some(param_name), ty, _) => {
param_name.as_str() == name.as_str()
&& matches!(ty, Type::Any(AnyStyle::Explicit))
}
_ => false,
}),
AnnotationTarget::ArgsParam(_) => items.iter().any(|p| {
matches!(p, Param::Varargs(_, ty) if matches!(ty, Type::Any(AnyStyle::Explicit)))
}),
AnnotationTarget::KwargsParam(_) => items.iter().any(|p| {
matches!(p, Param::Kwargs(_, ty) if matches!(ty, Type::Any(AnyStyle::Explicit)))
}),
_ => false,
}
})
}

/// Type check a delete expression, including ensuring that the target of the
/// delete is legal.
fn check_del_statement(&self, delete_target: &Expr, errors: &ErrorCollector) {
Expand Down
16 changes: 11 additions & 5 deletions pyrefly/lib/binding/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ assert_words!(KeyUndecoratedFunction, 1);
assert_words!(Binding, 4);
assert_words!(BindingExpect, 12);
assert_words!(BindingTypeAlias, 6);
assert_words!(BindingAnnotation, 11);
assert_words!(BindingAnnotation, 13);
assert_words!(BindingClass, 10);
assert_words!(BindingTParams, 9);
assert_words!(BindingClassBaseType, 3);
Expand Down Expand Up @@ -3232,22 +3232,28 @@ impl AnnotationTarget {
pub enum BindingAnnotation {
/// The type is annotated to be this key, will have the outer type removed.
/// Optionally occurring within a class, in which case Self refers to this class.
AnnotateExpr(AnnotationTarget, Expr, Option<Idx<KeyClass>>),
/// For a parameter/return annotation of an `@override` method, the final field
/// is the method's name, used to exempt annotations dictated by the base
/// signature (e.g. from `explicit-any`); `None` otherwise.
AnnotateExpr(AnnotationTarget, Expr, Option<Idx<KeyClass>>, Option<Name>),
/// A special form declaration like `Literal: _SpecialForm`.
SpecialForm(AnnotationTarget, SpecialForm),
}

impl DisplayWith<Bindings> for BindingAnnotation {
fn fmt(&self, f: &mut fmt::Formatter<'_>, ctx: &Bindings) -> fmt::Result {
match self {
Self::AnnotateExpr(target, x, class_key) => write!(
Self::AnnotateExpr(target, x, class_key, override_method) => write!(
f,
"AnnotateExpr({target}, {}, {})",
"AnnotateExpr({target}, {}, {}, {})",
ctx.module().display(x),
match class_key {
None => "None".to_owned(),
Some(t) => ctx.display(*t).to_string(),
}
},
override_method
.as_ref()
.map_or_else(|| "None".to_owned(), ToString::to_string),
),
Self::SpecialForm(target, sf) => write!(f, "SpecialForm({target}, {sf})"),
}
Expand Down
8 changes: 7 additions & 1 deletion pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,7 @@ impl<'a> BindingsBuilder<'a> {
undecorated_idx: Idx<KeyUndecoratedFunction>,
class_key: Option<Idx<KeyClass>>,
is_variadic: bool,
override_method: Option<Name>,
ignore_annotation: bool,
) {
let name = x.name();
Expand All @@ -2385,7 +2386,12 @@ impl<'a> BindingsBuilder<'a> {
x.annotation().map(|x| {
self.insert_binding(
KeyAnnotation::Annotation(ShortIdentifier::new(name)),
BindingAnnotation::AnnotateExpr(target.clone(), x.clone(), class_key),
BindingAnnotation::AnnotateExpr(
target.clone(),
x.clone(),
class_key,
override_method,
),
)
})
};
Expand Down
1 change: 1 addition & 0 deletions pyrefly/lib/binding/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ impl<'a> BindingsBuilder<'a> {
AnnotationTarget::ClassMember(member_name.clone()),
annotation_expr,
None,
None,
),
)
});
Expand Down
Loading
Loading