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
39 changes: 35 additions & 4 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pub enum CallStyle<'a> {
pub enum ConstructorKind {
// `MyClass`
BareClassName,
// `MyClass[int]`
SpecializedClass,
// `type[MyClass]`
TypeOfClass,
// `type[Self]`
Expand Down Expand Up @@ -1459,7 +1461,11 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
);
}
let metadata = self.get_metadata_for_class(cls.class_object());
if metadata.is_protocol() && constructor_kind == ConstructorKind::BareClassName {
let is_direct_class = matches!(
&constructor_kind,
ConstructorKind::BareClassName | ConstructorKind::SpecializedClass
);
if metadata.is_protocol() && is_direct_class {
self.error_with_context(
errors,
arguments_range,
Expand All @@ -1474,9 +1480,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
let abstract_members = self.get_abstract_members_for_class(cls.class_object());
let unimplemented_abstract_methods =
abstract_members.unimplemented_abstract_methods();
if constructor_kind == ConstructorKind::BareClassName
&& !unimplemented_abstract_methods.is_empty()
{
if is_direct_class && !unimplemented_abstract_methods.is_empty() {
self.error_with_context(
errors,
arguments_range,
Expand Down Expand Up @@ -2113,6 +2117,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
&self,
x: &ExprCall,
mut callee_ty: Type,
direct_class_specialization: bool,
hint: Option<HintRef>,
errors: &ErrorCollector,
) -> Type {
Expand Down Expand Up @@ -2406,6 +2411,32 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
hint,
errors,
),
_ if direct_class_specialization => {
let mut call_target = self.as_call_target_or_error(
ty.clone(),
CallStyle::FreeForm,
x.func.range(),
errors,
None,
);
if let CallTarget::Class(_, constructor_kind, _) = &mut call_target
&& *constructor_kind == ConstructorKind::TypeOfClass
{
*constructor_kind = ConstructorKind::SpecializedClass;
}
self.call_infer_with_callee_range(
call_target,
&args,
&kws,
x.arguments.range(),
Some(x.func.range()),
errors,
errors,
None,
hint,
None,
)
}
_ => self.freeform_call_infer(ty.clone(), &args, &kws, x.func.range(), x.arguments.range(), hint, errors),
}});
// TypeIs and TypeGuard functions return bool at runtime
Expand Down
48 changes: 38 additions & 10 deletions pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,16 @@ pub enum TypeOrExpr<'a> {

pub(crate) enum PreparedExprCall {
Resolved(Type),
Callee(Type),
Callee {
ty: Type,
direct_class_specialization: bool,
},
}

impl PreparedExprCall {
pub(crate) fn callee(&self) -> Option<&Type> {
match self {
Self::Callee(callee) => Some(callee),
Self::Callee { ty, .. } => Some(ty),
Self::Resolved(_) => None,
}
}
Expand Down Expand Up @@ -939,7 +942,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
// Infer a method call's receiver once. A Polars column-algebra transform is a
// pure function of the receiver schema and is dispatched here; any other
// receiver is reused for ordinary callee inference rather than inferred again.
let callee_ty = if let Expr::Attribute(func) = &*x.func {
let (callee_ty, direct_class_specialization) = if let Expr::Attribute(func) = &*x.func {
let base = self.expr_infer_impl(&func.value, None, errors);
if let Some(ty) = self.polars_select(base.ty(), func, &x.arguments, errors) {
return PreparedExprCall::Resolved(ty);
Expand All @@ -961,11 +964,27 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
// and deprecation check here as that path would for any other expression.
self.check_for_deprecated_call(attr.ty(), func.range(), errors);
self.record_type_trace(func.range(), attr.ty());
attr.into_ty()
(attr.into_ty(), false)
} else if let Expr::Subscript(func) = &*x.func {
let base = self.expr_infer_impl(&func.value, None, errors);
let callee = self.subscript_infer(&base, &func.slice, func.range(), errors);
let direct_class_specialization = matches!(
base.ty(),
Type::ClassDef(cls) if !self.get_class_tparams(cls).is_empty()
) && matches!(
callee.ty(),
Type::Type(inner) if matches!(&**inner, Type::ClassType(_))
);
self.check_for_deprecated_call(callee.ty(), func.range(), errors);
self.record_type_trace(func.range(), callee.ty());
(callee.into_ty(), direct_class_specialization)
} else {
self.expr_infer(&x.func, errors)
(self.expr_infer(&x.func, errors), false)
};
PreparedExprCall::Callee(callee_ty)
PreparedExprCall::Callee {
ty: callee_ty,
direct_class_specialization,
}
}

pub(crate) fn finish_prepared_expr_call(
Expand All @@ -975,9 +994,12 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
hint: Option<HintRef>,
errors: &ErrorCollector,
) -> Type {
let callee_ty = match prepared {
let (callee_ty, direct_class_specialization) = match prepared {
PreparedExprCall::Resolved(ty) => return ty,
PreparedExprCall::Callee(callee_ty) => callee_ty,
PreparedExprCall::Callee {
ty,
direct_class_specialization,
} => (ty, direct_class_specialization),
};

self.check_pytorch_tensor_item_call(x, &callee_ty, errors);
Expand Down Expand Up @@ -1022,12 +1044,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
} else {
obj_ty
.at_facet(&facet, || {
self.expr_call_infer(x, callee_ty.clone(), hint, errors)
self.expr_call_infer(
x,
callee_ty.clone(),
direct_class_specialization,
hint,
errors,
)
})
.into_ty()
}
} else {
self.expr_call_infer(x, callee_ty, hint, errors)
self.expr_call_infer(x, callee_ty, direct_class_specialization, hint, errors)
}
}

Expand Down
32 changes: 32 additions & 0 deletions pyrefly/lib/test/abstract_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ shape = Shape() # E: Cannot instantiate `Shape`
"#,
);

testcase!(
test_generic_abstract_class_instantiation_error,
r#"
from abc import ABC, abstractmethod

class Shape[T](ABC):
@abstractmethod
def area(self) -> T:
pass

class ConcreteShape(Shape[int]):
def area(self) -> int:
return 0

shape = Shape[int]() # E: Cannot instantiate `Shape`

def construct(cls: type[Shape[int]]) -> Shape[int]:
return cls()

classes: tuple[type[Shape[int]], ...] = (ConcreteShape,)
concrete_shape = classes[0]()
"#,
);

testcase!(
test_concrete_subclass_instantiation_ok,
r#"
Expand Down Expand Up @@ -59,10 +83,18 @@ class DirectABCMeta(metaclass=ABCMeta):
class IndirectABCMeta(DirectABCMeta):
pass

class DirectGenericABC[T](ABC):
pass

class DirectGenericABCMeta[T](metaclass=ABCMeta):
pass

direct_abc = DirectABC() # E: Cannot instantiate `DirectABC`
indirect_abc = IndirectABC()
direct_abc_meta = DirectABCMeta() # E: Cannot instantiate `DirectABCMeta`
indirect_abc_meta = IndirectABCMeta()
direct_generic_abc = DirectGenericABC[int]()
direct_generic_abc_meta = DirectGenericABCMeta[int]()
"#,
);

Expand Down
Loading