diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 4a29a10e7b..b4139f2cf7 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -908,12 +908,34 @@ Bad: type[Unrelated] = type("Bad", (Base,), {}) ## Special base classes Some special base classes work with dynamic class creation, but special semantics may not be fully -synthesized: +synthesized. + +### Invalid special bases + +Dynamic classes cannot directly inherit from `Generic`, `Protocol`, or `TypedDict`. These special +forms require class syntax for their semantics to be properly applied: + +```py +from typing import Generic, Protocol, TypeVar +from typing_extensions import TypedDict + +T = TypeVar("T") + +# error: [invalid-base] "Invalid base for class created via `type()`" +GenericClass = type("GenericClass", (Generic[T],), {}) + +# error: [unsupported-dynamic-base] "Unsupported base for class created via `type()`" +ProtocolClass = type("ProtocolClass", (Protocol,), {}) + +# error: [invalid-base] "Invalid base for class created via `type()`" +TypedDictClass = type("TypedDictClass", (TypedDict,), {}) +``` ### Protocol bases +Inheriting from a class that is itself a protocol is valid: + ```py -# Protocol bases work - the class is created as a subclass of the protocol from typing import Protocol from ty_extensions import reveal_mro @@ -930,8 +952,9 @@ reveal_type(instance) # revealed: ProtoImpl ### TypedDict bases +Inheriting from a class that is itself a TypedDict is valid: + ```py -# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses from typing_extensions import TypedDict from ty_extensions import reveal_mro @@ -964,26 +987,26 @@ reveal_mro(Point3D) # revealed: (, , - -# Even empty enums fail - EnumMeta requires special dict handling -# TODO: We should emit a diagnostic here too -ValidExtension = type("ValidExtension", (EmptyEnum,), {}) -reveal_type(ValidExtension) # revealed: +# Empty enums fail because EnumMeta requires special dict handling +# error: [invalid-base] "Invalid base for class created via `type()`" +InvalidExtension = type("InvalidExtension", (EmptyEnum,), {}) ``` ## `__init_subclass__` keyword arguments @@ -1046,3 +1069,27 @@ reveal_type(Dynamic) # revealed: # Metaclass attributes are accessible on the class reveal_type(Dynamic.custom_attr) # revealed: str ``` + +## `final()` on dynamic classes + +Using `final()` as a function (not a decorator) on dynamic classes has no effect. The class is +passed through unchanged: + +```py +from typing import final + +# TODO: Add a diagnostic for ineffective use of `final()` here. +FinalClass = final(type("FinalClass", (), {})) +reveal_type(FinalClass) # revealed: + +# Subclassing is allowed because `final()` as a function has no effect +class Child(FinalClass): ... + +# Same with base classes +class Base: ... + +# TODO: Add a diagnostic for ineffective use of `final()` here. +FinalDerived = final(type("FinalDerived", (Base,), {})) + +class Child2(FinalDerived): ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md new file mode 100644 index 0000000000..d8022ac117 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md @@ -0,0 +1,78 @@ +# Unsupported base for dynamic `type()` classes + + + +## `@final` class + +Classes decorated with `@final` cannot be subclassed: + +```py +from typing import final + +@final +class FinalClass: + pass + +X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class] +``` + +## `Generic` base + +Dynamic classes created via `type()` cannot inherit from `Generic`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +X = type("X", (Generic[T],), {}) # error: [invalid-base] +``` + +## `Protocol` base + +Dynamic classes created via `type()` cannot inherit from `Protocol`: + +```py +from typing import Protocol + +X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base] +``` + +## `TypedDict` base + +Dynamic classes created via `type()` cannot inherit from `TypedDict` directly. Use +`TypedDict("Name", ...)` instead: + +```py +from typing_extensions import TypedDict + +X = type("X", (TypedDict,), {}) # error: [invalid-base] +``` + +## Enum base + +Dynamic classes created via `type()` cannot inherit from Enum classes because `EnumMeta` expects +special dict attributes that `type()` doesn't provide: + +```py +from enum import Enum + +class MyEnum(Enum): + pass + +X = type("X", (MyEnum,), {}) # error: [invalid-base] +``` + +## Enum with members + +Enums with members are final and cannot be subclassed at all: + +```py +from enum import Enum + +class Color(Enum): + RED = 1 + GREEN = 2 + +X = type("X", (Color,), {}) # error: [subclass-of-final-class] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_base_(4873196c8b48364).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_base_(4873196c8b48364).snap new file mode 100644 index 0000000000..a5c91fe22a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_base_(4873196c8b48364).snap @@ -0,0 +1,39 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum base +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from enum import Enum +2 | +3 | class MyEnum(Enum): +4 | pass +5 | +6 | X = type("X", (MyEnum,), {}) # error: [invalid-base] +``` + +# Diagnostics + +``` +error[invalid-base]: Invalid base for class created via `type()` + --> src/mdtest_snippet.py:6:16 + | +4 | pass +5 | +6 | X = type("X", (MyEnum,), {}) # error: [invalid-base] + | ^^^^^^ Has type `` + | +info: Creating an enum class via `type()` is not supported +info: Consider using `Enum("X", [])` instead +info: rule `invalid-base` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_with_members_(81bef9a8e1230854).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_with_members_(81bef9a8e1230854).snap new file mode 100644 index 0000000000..df86f8d622 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_Enum_with_members_(81bef9a8e1230854).snap @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum with members +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from enum import Enum +2 | +3 | class Color(Enum): +4 | RED = 1 +5 | GREEN = 2 +6 | +7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class] +``` + +# Diagnostics + +``` +error[subclass-of-final-class]: Class `X` cannot inherit from final class `Color` + --> src/mdtest_snippet.py:7:16 + | +5 | GREEN = 2 +6 | +7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class] + | ^^^^^ + | +info: rule `subclass-of-final-class` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`@final`_class_(ea69d237256b3762).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`@final`_class_(ea69d237256b3762).snap new file mode 100644 index 0000000000..b3fc3f9d26 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`@final`_class_(ea69d237256b3762).snap @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `@final` class +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import final +2 | +3 | @final +4 | class FinalClass: +5 | pass +6 | +7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class] +``` + +# Diagnostics + +``` +error[subclass-of-final-class]: Class `X` cannot inherit from final class `FinalClass` + --> src/mdtest_snippet.py:7:16 + | +5 | pass +6 | +7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class] + | ^^^^^^^^^^ + | +info: rule `subclass-of-final-class` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Generic`_base_(d455f46a27cec685).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Generic`_base_(d455f46a27cec685).snap new file mode 100644 index 0000000000..0c7718cebd --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Generic`_base_(d455f46a27cec685).snap @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Generic` base +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import Generic, TypeVar +2 | +3 | T = TypeVar("T") +4 | +5 | X = type("X", (Generic[T],), {}) # error: [invalid-base] +``` + +# Diagnostics + +``` +error[invalid-base]: Invalid base for class created via `type()` + --> src/mdtest_snippet.py:5:16 + | +3 | T = TypeVar("T") +4 | +5 | X = type("X", (Generic[T],), {}) # error: [invalid-base] + | ^^^^^^^^^^ Has type `` + | +info: Classes created via `type()` cannot be generic +info: Consider using `class X(Generic[...]): ...` instead +info: rule `invalid-base` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Protocol`_base_(99c9bde73664dd51).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Protocol`_base_(99c9bde73664dd51).snap new file mode 100644 index 0000000000..f423adcda2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`Protocol`_base_(99c9bde73664dd51).snap @@ -0,0 +1,36 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Protocol` base +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import Protocol +2 | +3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base] +``` + +# Diagnostics + +``` +info[unsupported-dynamic-base]: Unsupported base for class created via `type()` + --> src/mdtest_snippet.py:3:16 + | +1 | from typing import Protocol +2 | +3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base] + | ^^^^^^^^ Has type `` + | +info: Classes created via `type()` cannot be protocols +info: Consider using `class X(Protocol): ...` instead +info: rule `unsupported-dynamic-base` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`TypedDict`_base_(6f76171c88fc8760).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`TypedDict`_base_(6f76171c88fc8760).snap new file mode 100644 index 0000000000..8899b255d4 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn…_-_Unsupported_base_for…_-_`TypedDict`_base_(6f76171c88fc8760).snap @@ -0,0 +1,36 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `TypedDict` base +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing_extensions import TypedDict +2 | +3 | X = type("X", (TypedDict,), {}) # error: [invalid-base] +``` + +# Diagnostics + +``` +error[invalid-base]: Invalid base for class created via `type()` + --> src/mdtest_snippet.py:3:16 + | +1 | from typing_extensions import TypedDict +2 | +3 | X = type("X", (TypedDict,), {}) # error: [invalid-base] + | ^^^^^^^^^ Has type `` + | +info: Classes created via `type()` cannot be TypedDicts +info: Consider using `TypedDict("X", {})` instead +info: rule `invalid-base` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d23e4c219c..f2b4142230 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -562,25 +562,16 @@ impl<'db> ClassLiteral<'db> { } /// Returns the generic context if this is a generic class. - /// - // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts - // to inherit from `Generic[T]`, since dynamic classes can't be generic. See also: `is_protocol`. pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { self.as_static().and_then(|class| class.generic_context(db)) } /// Returns whether this class is a protocol. - /// - // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts - // to inherit from `Protocol`, since dynamic classes can't be protocols. See also: `generic_context`. pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { self.as_static().is_some_and(|class| class.is_protocol(db)) } /// Returns whether this class is a `TypedDict`. - // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts - // to inherit from `TypedDict`. To create a dynamic TypedDict, you should invoke - // `TypedDict` as a function, not `type`. See also: `generic_context`, `is_protocol`. pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.is_typed_dict(db), @@ -634,10 +625,12 @@ impl<'db> ClassLiteral<'db> { } /// Returns whether this class is final. - // TODO: Support `@final` on dynamic classes, e.g. `X = final(type("X", (), {}))`. - // We should either recognize and track this, or emit a diagnostic if unsupported. pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { - self.as_static().is_some_and(|class| class.is_final(db)) + match self { + Self::Static(class) => class.is_final(db), + // Dynamic classes created via `type()` cannot be marked as final. + Self::Dynamic(_) => false, + } } /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`, diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index f5067cc675..e3f3e9b083 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -67,7 +67,6 @@ pub(crate) fn enum_metadata<'db>( // // MyEnum = type("MyEnum", (BaseEnum,), {"A": 1, "B": 2}) // ``` - // TODO: Add a diagnostic for including an enum in a `type(...)` call. return None; } }; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 945ee83b0f..7c7ce9ad3a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6443,26 +6443,129 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .iter() .enumerate() .map(|(idx, base)| { + let diagnostic_node = bases_tuple_elts + .and_then(|elts| elts.get(idx)) + .unwrap_or(bases_node); + // First try the standard conversion. if let Some(class_base) = ClassBase::try_from_type(db, *base, placeholder_class) { - // Collect disjoint bases for instance-layout-conflict checking. - if let ClassBase::Class(base_class) = class_base { - if let Some(disjoint_base) = base_class.nearest_disjoint_base(db) { - disjoint_bases.insert( - disjoint_base, - idx, - base_class.class_literal(db), - ); + // Check for special bases that are not allowed for dynamic classes. + // Dynamic classes can't be generic, protocols, TypedDicts, or enums. + match class_base { + ClassBase::Generic | ClassBase::TypedDict => { + if let Some(builder) = + self.context.report_lint(&INVALID_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic( + "Invalid base for class created via `type()`", + ); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base.display(db) + )); + match class_base { + ClassBase::Generic => { + diagnostic.info( + "Classes created via `type()` cannot be generic", + ); + diagnostic.info(format_args!( + "Consider using `class {name}(Generic[...]): ...` instead" + )); + } + ClassBase::TypedDict => { + diagnostic.info( + "Classes created via `type()` cannot be TypedDicts", + ); + diagnostic.info(format_args!( + "Consider using `TypedDict(\"{name}\", {{}})` instead" + )); + } + _ => unreachable!(), + } + } + return ClassBase::unknown(); } - } - return class_base; - } + ClassBase::Protocol => { + if let Some(builder) = self + .context + .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic( + "Unsupported base for class created via `type()`", + ); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base.display(db) + )); + diagnostic.info( + "Classes created via `type()` cannot be protocols", + ); + diagnostic.info(format_args!( + "Consider using `class {name}(Protocol): ...` instead" + )); + } + return ClassBase::unknown(); + } + ClassBase::Class(class_type) => { + // Check if base is @final (includes enums with members). + if class_type.is_final(db) { + if let Some(builder) = self + .context + .report_lint(&SUBCLASS_OF_FINAL_CLASS, diagnostic_node) + { + builder.into_diagnostic(format_args!( + "Class `{name}` cannot inherit from final class `{}`", + class_type.name(db) + )); + } + return ClassBase::unknown(); + } - let diagnostic_node = bases_tuple_elts - .and_then(|elts| elts.get(idx)) - .unwrap_or(bases_node); + // Enum subclasses require the EnumMeta metaclass, which + // expects special dict attributes that `type()` doesn't provide. + if let Some((static_class, _)) = + class_type.static_class_literal(db) + { + if is_enum_class_by_inheritance(db, static_class) { + if let Some(builder) = self + .context + .report_lint(&INVALID_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic( + "Invalid base for class created via `type()`", + ); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base.display(db) + )); + diagnostic.info( + "Creating an enum class via `type()` is not supported", + ); + diagnostic.info(format_args!( + "Consider using `Enum(\"{name}\", [])` instead" + )); + } + return ClassBase::unknown(); + } + } + + // Collect disjoint bases for instance-layout-conflict checking. + if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) + { + disjoint_bases.insert( + disjoint_base, + idx, + class_type.class_literal(db), + ); + } + + return class_base; + } + ClassBase::Dynamic(_) => return class_base, + } + } // If that fails, check if the type is "type-like" (e.g., `type[Base]`). // For type-like bases we emit `unsupported-dynamic-base` and use