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
10 changes: 10 additions & 0 deletions conformance/third_party/conformance.exp
Original file line number Diff line number Diff line change
Expand Up @@ -8187,6 +8187,16 @@
"name": "bad-assignment",
"stop_column": 36,
"stop_line": 56
},
{
"code": -2,
"column": 17,
"concise_description": "Cannot instantiate abstract class `Concrete7B`",
"description": "Cannot instantiate abstract class `Concrete7B`",
"line": 172,
"name": "bad-instantiation",
"stop_column": 19,
"stop_line": 172
}
],
"protocols_generic.py": [
Expand Down
3 changes: 2 additions & 1 deletion conformance/third_party/conformance.result
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@
"Line 90: Expected 1 errors",
"Line 110: Expected 1 errors",
"Line 135: Expected 1 errors",
"Line 165: Expected 1 errors"
"Line 165: Expected 1 errors",
"Line 172: Unexpected errors ['Cannot instantiate abstract class `Concrete7B`']"
],
"protocols_generic.py": [
"Line 144: Unexpected errors ['`ConcreteHasProperty1` is not assignable to `HasPropertyProto`']"
Expand Down
2 changes: 1 addition & 1 deletion conformance/third_party/results.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"overloads_evaluation.py": 14,
"protocols_class_objects.py": 7,
"protocols_definition.py": 3,
"protocols_explicit.py": 6,
"protocols_explicit.py": 7,
"protocols_generic.py": 1,
"protocols_merging.py": 1,
"protocols_modules.py": 1,
Expand Down
2 changes: 2 additions & 0 deletions crates/pyrefly_types/src/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ pub struct FuncFlags {
pub has_enum_member_decoration: bool,
pub is_override: bool,
pub has_final_decoration: bool,
/// A function decorated with `@abc.abstractmethod`
pub is_abstract_method: bool,
/// A function decorated with `typing.dataclass_transform(...)`, turning it into a
/// `dataclasses.dataclass`-like decorator. Stores the keyword values passed to the
/// `dataclass_transform` call. See
Expand Down
12 changes: 12 additions & 0 deletions pyrefly/lib/alt/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
hint: Option<&Type>,
) -> Type {
// Based on https://typing.readthedocs.io/en/latest/spec/constructors.html.
let instance_ty = Type::ClassType(cls.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

this appears unused?


// Check if trying to instantiate an abstract class
if self.has_abstract_methods(&cls) {
self.error(
errors,
range,
ErrorInfo::Kind(ErrorKind::BadInstantiation),
format!("Cannot instantiate abstract class `{}`", cls.name()),
);
}

if let Some(hint) = hint {
self.solver()
.freshen_class_targs(cls.targs_mut(), self.uniques);
Expand Down
113 changes: 113 additions & 0 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1782,4 +1782,117 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
.and_then(|ty| make_bound_method(Type::type_form(cls.clone().to_type()), ty).ok())
}
}

/// Check if a class inherits from abc.ABC
///
/// This recursively traverses the class hierarchy to determine if the class
/// inherits from Python's abstract base class (abc.ABC). Only classes that
/// inherit from abc.ABC can be considered abstract in Python.
fn inherits_from_abc(&self, cls: &ClassType) -> bool {
let class_obj = cls.class_object();

// Check if this class is abc.ABC itself
if class_obj.module_name().as_str() == "abc" && class_obj.name().as_str() == "ABC" {
return true;
}

// Recursively check base classes
let metadata = self.get_metadata_for_class(class_obj);
Copy link
Contributor

@yangdanny97 yangdanny97 Aug 6, 2025

Choose a reason for hiding this comment

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

I think we can pre-calculate inherits_from_abc and store it in class metadata. cc @grievejia

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this idea, we could do something like:

pub struct ClassMetadata {
       // ... existing fields ...
       inherits_from_abc: bool,  // New field
}

And then we could populate ABC Inheritance during class metadata creation.

for base in metadata.bases_with_metadata() {
if self.inherits_from_abc(&base.0) {
return true;
}
}

false
}

/// Check if a class has any unimplemented abstract methods
///
/// Returns true if the class:
/// 1. Inherits from abc.ABC (or is abc.ABC itself)
/// 2. Has abstract methods in its hierarchy that are not implemented
///
/// This method is used to prevent instantiation of abstract classes.
pub fn has_abstract_methods(&self, cls: &ClassType) -> bool {
// Only classes that inherit from abc.ABC can be abstract
if !self.inherits_from_abc(cls) {
return false;
}

// Check for unimplemented abstract methods with early exit
self.has_unimplemented_abstract_methods(cls)
}

/// Check if a class has unimplemented abstract methods
fn has_unimplemented_abstract_methods(&self, cls: &ClassType) -> bool {
let mut abstract_methods = SmallSet::new();

// Collect all abstract methods from the hierarchy
self.collect_abstract_methods_from_class(cls, &mut abstract_methods);

// Check each abstract method for concrete implementation
for method_name in &abstract_methods {
if !self.class_provides_concrete_implementation(cls, method_name) {
return true;
}
}

false
}

/// Recursively collect abstract methods from a class and its bases
fn collect_abstract_methods_from_class(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we need to traverse all the base classes like this. If we look up each field the class has like so

fn get_class_member_impl(
it would handle the lookup, and we can check if the field it returns is abstract or not.

Of course, this relies on the MRO being correct and able to handle cases when one base class declares something as abstract and another base class provides the impl. @stroxler would know more about this

&self,
cls: &ClassType,
abstract_methods: &mut SmallSet<Name>,
) {
let class_obj = cls.class_object();
let metadata = self.get_metadata_for_class(class_obj);

// First collect from base classes
for base in metadata.bases_with_metadata() {
self.collect_abstract_methods_from_class(&base.0, abstract_methods);
}

// Then collect abstract methods from this class
for field_name in class_obj.fields() {
if let Some(field) = self.get_from_class(
class_obj,
&KeyClassField(class_obj.index(), field_name.clone()),
) {
match &field.0 {
ClassFieldInner::Simple { ty, .. } => {
if let Type::Function(function) = ty {
if function.metadata.flags.is_abstract_method {
Copy link
Contributor

Choose a reason for hiding this comment

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

we might need to handle overloaded functions too

abstract_methods.insert(field_name.clone());
}
}
}
}
}
}
}

/// Check if a class provides a concrete (non-abstract) implementation of a method
fn class_provides_concrete_implementation(&self, cls: &ClassType, method_name: &Name) -> bool {
let class_obj = cls.class_object();

// Check if the class has this method
if let Some(field) = self.get_from_class(
class_obj,
&KeyClassField(class_obj.index(), method_name.clone()),
) {
match &field.0 {
ClassFieldInner::Simple { ty, .. } => {
if let Type::Function(function) = ty {
// Method exists - check if it's concrete (not abstract)
return !function.metadata.flags.is_abstract_method;
}
}
}
}

false // Method not found or not a function
}
}
6 changes: 6 additions & 0 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
let mut has_enum_member_decoration = false;
let mut is_override = false;
let mut has_final_decoration = false;
let mut is_abstract_method = false;
let mut dataclass_transform_metadata = None;
let decorators = decorators
.iter()
Expand Down Expand Up @@ -236,6 +237,10 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
has_final_decoration = true;
false
}
Some(CalleeKind::Function(FunctionKind::AbstractMethod)) => {
is_abstract_method = true;
false
}
_ if matches!(decorator_ty, Type::ClassType(cls) if cls.has_qname("warnings", "deprecated")) => {
is_deprecated = true;
false
Expand Down Expand Up @@ -504,6 +509,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
has_enum_member_decoration,
is_override,
has_final_decoration,
is_abstract_method,
dataclass_transform_metadata,
},
};
Expand Down
103 changes: 103 additions & 0 deletions pyrefly/lib/test/abstract_class_instantiation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

use crate::testcase;

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

class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass

# This should error - cannot instantiate abstract class
shape = Shape() # E: Cannot instantiate abstract class `Shape`
"#,
);

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

class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass

class Circle(Shape):
def area(self) -> float:
return 3.14

# This should work - concrete subclass can be instantiated
circle = Circle()
"#,
);

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

class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass

class Circle(Shape):
def area(self) -> float:
return 3.14

def calculate_area(shape: Shape) -> float:
# This should work - polymorphic call is allowed
return shape.area()

circle = Circle()
area = calculate_area(circle)
"#,
);

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

class Drawable(ABC):
@abstractmethod
def draw(self) -> None:
pass

@abstractmethod
def erase(self) -> None:
pass

# This should error - class has multiple abstract methods
drawable = Drawable() # E: Cannot instantiate abstract class `Drawable`
"#,
);

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

class Base(ABC):
@abstractmethod
def method(self) -> None:
pass

class Child(Base):
# Child doesn't implement method, so it's still abstract
pass

# This should error - child class is still abstract
child = Child() # E: Cannot instantiate abstract class `Child`
"#,
);
1 change: 1 addition & 0 deletions pyrefly/lib/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#![cfg(test)]

mod abstract_class_instantiation;
mod assign;
mod attribute_narrow;
mod attributes;
Expand Down
9 changes: 8 additions & 1 deletion website/docs/error-kinds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,19 @@ For example, argument order is enforced by the parser, so `def f(x: int = 1, y:

## bad-instantiation

This error occurs when attempting to instantiate a class that cannot be instantiated, such as a protocol:
This error occurs when attempting to instantiate a class that cannot be instantiated, such as a protocol or an abstract class:

```python
from typing import Protocol
class C(Protocol): ...
C() # bad-instantiation

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
Shape() # bad-instantiation
```

## bad-keyword-argument
Expand Down
Loading