From 992a1af4c23c2bad0d7a9961ec3fe861381bb593 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 27 Mar 2025 17:30:56 -0400 Subject: [PATCH] [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 --- .../resources/mdtest/annotations/literal.md | 2 +- .../resources/mdtest/attributes.md | 31 +++++++++++++++++++ .../src/module_resolver/module.rs | 6 ++++ crates/red_knot_python_semantic/src/types.rs | 8 +++++ .../src/types/class.rs | 17 ++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md index b09b6f8701..97cd141494 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md @@ -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] diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index dda78f085d..b62aff8408 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/red_knot_python_semantic/src/module_resolver/module.rs index a761acf87e..1c5b1192f3 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/module.rs @@ -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 { diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3693455404..164f3849f1 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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, diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index d7accc8e53..156b521c20 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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