[red-knot] Reduce false positives on super() and enum-class attribute accesses (#17004)

## Summary

This PR adds some branches so that we infer `Todo` types for attribute
access on instances of `super()` and subtypes of `type[Enum]`. It reduces
false positives in the short term until we implement full support for
these features.

## Test Plan

New mdtests added + mypy_primer report
This commit is contained in:
Alex Waygood 2025-03-27 17:30:56 -04:00 committed by GitHub
parent c963b185eb
commit 992a1af4c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 63 additions and 1 deletions

View file

@ -36,7 +36,7 @@ def f():
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Unknown | Literal[0]
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
# error: [invalid-type-form]
invalid1: Literal[3 + 4]

View file

@ -1709,6 +1709,37 @@ reveal_type(C.a_type) # revealed: type
reveal_type(C.a_none) # revealed: None
```
## Enum classes
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
```py
import enum
reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes)
class Foo(enum.Enum):
BAR = 1
reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
```
## `super()`
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
```py
class Foo:
def bar(self) -> int:
return 42
class Bar(Foo):
def bar(self) -> int:
return super().bar()
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View file

@ -104,6 +104,7 @@ impl ModuleKind {
#[strum(serialize_all = "snake_case")]
pub enum KnownModule {
Builtins,
Enum,
Types,
#[strum(serialize = "_typeshed")]
Typeshed,
@ -121,6 +122,7 @@ impl KnownModule {
pub const fn as_str(self) -> &'static str {
match self {
Self::Builtins => "builtins",
Self::Enum => "enum",
Self::Types => "types",
Self::Typing => "typing",
Self::Typeshed => "_typeshed",
@ -164,6 +166,10 @@ impl KnownModule {
pub const fn is_inspect(self) -> bool {
matches!(self, Self::Inspect)
}
pub const fn is_enum(self) -> bool {
matches!(self, Self::Enum)
}
}
impl std::fmt::Display for KnownModule {

View file

@ -2018,6 +2018,10 @@ impl<'db> Type<'db> {
Symbol::bound(Type::IntLiteral(segment.into())).into()
}
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Super) => {
SymbolAndQualifiers::todo("super() support")
}
Type::IntLiteral(_) if matches!(name_str, "real" | "numerator") => {
Symbol::bound(self).into()
}
@ -2105,6 +2109,10 @@ impl<'db> Type<'db> {
return class_attr_plain;
}
if self.is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) {
return SymbolAndQualifiers::todo("Attribute access on enum classes");
}
let class_attr_fallback = Self::try_call_dunder_get_on_attribute(
db,
class_attr_plain,

View file

@ -827,6 +827,9 @@ pub enum KnownClass {
BaseException,
BaseExceptionGroup,
Classmethod,
Super,
// enum
Enum,
// Types
GenericAlias,
ModuleType,
@ -924,6 +927,8 @@ impl<'db> KnownClass {
| Self::Deque
| Self::Float
| Self::Sized
| Self::Enum
| Self::Super
| Self::Classmethod => Truthiness::Ambiguous,
}
}
@ -970,6 +975,8 @@ impl<'db> KnownClass {
Self::Deque => "deque",
Self::Sized => "Sized",
Self::OrderedDict => "OrderedDict",
Self::Enum => "Enum",
Self::Super => "super",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
@ -1120,8 +1127,10 @@ impl<'db> KnownClass {
| Self::Classmethod
| Self::Slice
| Self::Range
| Self::Super
| Self::Property => KnownModule::Builtins,
Self::VersionInfo => KnownModule::Sys,
Self::Enum => KnownModule::Enum,
Self::GenericAlias
| Self::ModuleType
| Self::FunctionType
@ -1212,6 +1221,8 @@ impl<'db> KnownClass {
| Self::ParamSpec
| Self::TypeVarTuple
| Self::Sized
| Self::Enum
| Self::Super
| Self::NewType => false,
}
}
@ -1265,6 +1276,8 @@ impl<'db> KnownClass {
| Self::ParamSpec
| Self::TypeVarTuple
| Self::Sized
| Self::Enum
| Self::Super
| Self::NewType => false,
}
}
@ -1318,6 +1331,8 @@ impl<'db> KnownClass {
"_NoDefaultType" => Self::NoDefaultType,
"SupportsIndex" => Self::SupportsIndex,
"Sized" => Self::Sized,
"Enum" => Self::Enum,
"super" => Self::Super,
"_version_info" => Self::VersionInfo,
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
Self::EllipsisType
@ -1368,6 +1383,8 @@ impl<'db> KnownClass {
| Self::FunctionType
| Self::MethodType
| Self::MethodWrapperType
| Self::Enum
| Self::Super
| Self::WrapperDescriptorType => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm