mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Support 'dangling' type(...) constructors (#22537)
## Summary
This PR adds support for 'dangling' `type(...)` constructors, e.g.:
```python
class Foo(type("Bar", ...)):
...
```
As opposed to:
```python
Bar = type("Bar", ...)
```
The former doesn't have a `Definition` since it doesn't get bound to a
place, so we instead need to store the `NodeIndex`. Per @MichaReiser's
suggestion, we can use a Salsa tracked struct for this.
This commit is contained in:
@@ -1649,6 +1649,65 @@ Traceb<CURSOR>ackType
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
/// goto-definition on a dynamic class literal (created via `type()`)
|
||||
#[test]
|
||||
fn goto_definition_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
DynClass = type("DynClass", (), {})
|
||||
|
||||
x = DynCla<CURSOR>ss()
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Go to definition
|
||||
--> main.py:4:5
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
| ^^^^^^^^ Clicking here
|
||||
|
|
||||
info: Found 2 definitions
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
| --------
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
|
|
||||
::: stdlib/builtins.pyi:137:9
|
||||
|
|
||||
135 | def __class__(self, type: type[Self], /) -> None: ...
|
||||
136 | def __init__(self) -> None: ...
|
||||
137 | def __new__(cls) -> Self: ...
|
||||
| -------
|
||||
138 | # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers.
|
||||
139 | # Overriding them in subclasses has different semantics, even if the override has an identical signature.
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a dangling dynamic class literal (not assigned to a variable)
|
||||
#[test]
|
||||
fn goto_definition_dangling_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
class Foo(type("Ba<CURSOR>r", (), {})):
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
// TODO: Should only list `a: int`
|
||||
#[test]
|
||||
fn redeclarations() {
|
||||
|
||||
@@ -20,16 +20,13 @@ class Base: ...
|
||||
class Mixin: ...
|
||||
|
||||
# We synthesize a class type using the name argument
|
||||
Foo = type("Foo", (), {})
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
|
||||
|
||||
# With a single base class
|
||||
Foo2 = type("Foo", (Base,), {"attr": 1})
|
||||
reveal_type(Foo2) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
|
||||
|
||||
# With multiple base classes
|
||||
Foo3 = type("Foo", (Base, Mixin), {})
|
||||
reveal_type(Foo3) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
|
||||
|
||||
# The inferred type is assignable to type[Base] since Foo inherits from Base
|
||||
tests: list[type[Base]] = []
|
||||
@@ -514,24 +511,22 @@ reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types.
|
||||
|
||||
Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which
|
||||
produces slightly different error messages than assigned dynamic class creation:
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
|
||||
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
@@ -598,6 +593,19 @@ class Y(C, B): ...
|
||||
Conflict = type("Conflict", (X, Y), {})
|
||||
```
|
||||
|
||||
## MRO error highlighting (snapshot)
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
This snapshot test documents the diagnostic highlighting range for dynamic class literals.
|
||||
Currently, the entire `type()` call expression is highlighted:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
```
|
||||
|
||||
## Metaclass conflicts
|
||||
|
||||
Metaclass conflicts are detected and reported:
|
||||
@@ -877,20 +885,24 @@ def f(*args, **kwargs):
|
||||
|
||||
## Explicit type annotations
|
||||
|
||||
TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type.
|
||||
This will be fixed when we support all `type()` calls (including inline) via generic handling.
|
||||
When an explicit type annotation is provided, the inferred type is checked against it:
|
||||
|
||||
```py
|
||||
# The annotation `type` is compatible with the inferred class literal type
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: <class 'T'>
|
||||
|
||||
# The annotation `type[Base]` is compatible with the inferred type
|
||||
class Base: ...
|
||||
|
||||
# TODO: Should infer `<class 'T'>` instead of `type`
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: type
|
||||
|
||||
# TODO: Should infer `<class 'Derived'>` instead of `type[Base]}
|
||||
# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`"
|
||||
Derived: type[Base] = type("Derived", (Base,), {})
|
||||
reveal_type(Derived) # revealed: type[Base]
|
||||
reveal_type(Derived) # revealed: <class 'Derived'>
|
||||
|
||||
# Incompatible annotation produces an error
|
||||
class Unrelated: ...
|
||||
|
||||
# error: [invalid-assignment]
|
||||
Bad: type[Unrelated] = type("Bad", (Base,), {})
|
||||
```
|
||||
|
||||
## Special base classes
|
||||
|
||||
@@ -210,12 +210,15 @@ Narrowing does not occur in the same way if `type` is used to dynamically create
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# Inline type() calls fall back to regular type overload matching.
|
||||
# TODO: Once inline type() calls synthesize class types, this should narrow x to Never.
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
#
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
# But we synthesize a new class object as the result of a three-argument call to `type`,
|
||||
# and we know that this synthesized class object is not the same object as the `str` class object,
|
||||
# so here the type is narrowed to `Never`!
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: type.md - Calls to `type()` - MRO error highlighting (snapshot)
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/call/type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A: ...
|
||||
2 |
|
||||
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[duplicate-base]: Duplicate base class <class 'A'> in class `Dup`
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | class A: ...
|
||||
2 |
|
||||
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
@@ -938,6 +938,18 @@ impl DefinitionKind<'_> {
|
||||
| DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value expression for assignment-based definitions.
|
||||
///
|
||||
/// Returns `Some` for `Assignment` and `AnnotatedAssignment` (if it has a value),
|
||||
/// `None` for all other definition kinds.
|
||||
pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> {
|
||||
match self {
|
||||
DefinitionKind::Assignment(assignment) => Some(assignment.value(module)),
|
||||
DefinitionKind::AnnotatedAssignment(assignment) => assignment.value(module),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Hash, get_size2::GetSize)]
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
||||
|
||||
use ruff_db::{files::File, parsed::ParsedModuleRef};
|
||||
use ruff_index::newtype_index;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{self as ast, NodeIndex};
|
||||
|
||||
use crate::{
|
||||
Db,
|
||||
@@ -463,6 +463,27 @@ impl NodeWithScopeKind {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the anchor node index for this scope, or `None` for the module scope.
|
||||
///
|
||||
/// This is used to compute relative node indices for expressions within the scope,
|
||||
/// providing a stable anchor that only changes when the scope-introducing node changes.
|
||||
pub(crate) fn node_index(&self) -> Option<NodeIndex> {
|
||||
match self {
|
||||
Self::Module => None,
|
||||
Self::Class(class) => Some(class.index()),
|
||||
Self::ClassTypeParameters(class) => Some(class.index()),
|
||||
Self::Function(function) => Some(function.index()),
|
||||
Self::FunctionTypeParameters(function) => Some(function.index()),
|
||||
Self::TypeAlias(type_alias) => Some(type_alias.index()),
|
||||
Self::TypeAliasTypeParameters(type_alias) => Some(type_alias.index()),
|
||||
Self::Lambda(lambda) => Some(lambda.index()),
|
||||
Self::ListComprehension(comp) => Some(comp.index()),
|
||||
Self::SetComprehension(comp) => Some(comp.index()),
|
||||
Self::DictComprehension(comp) => Some(comp.index()),
|
||||
Self::GeneratorExpression(generator) => Some(generator.index()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
|
||||
@@ -6559,9 +6559,9 @@ impl<'db> Type<'db> {
|
||||
Some(TypeDefinition::Function(function.definition(db)))
|
||||
}
|
||||
Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))),
|
||||
Self::ClassLiteral(class_literal) => Some(class_literal.type_definition(db)),
|
||||
Self::ClassLiteral(class_literal) => class_literal.type_definition(db),
|
||||
Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))),
|
||||
Self::NominalInstance(instance) => Some(instance.class(db).type_definition(db)),
|
||||
Self::NominalInstance(instance) => instance.class(db).type_definition(db),
|
||||
Self::KnownInstance(instance) => match instance {
|
||||
KnownInstanceType::TypeVar(var) => {
|
||||
Some(TypeDefinition::TypeVar(var.definition(db)?))
|
||||
@@ -6575,7 +6575,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
SubclassOfInner::Dynamic(_) => None,
|
||||
SubclassOfInner::Class(class) => Some(class.type_definition(db)),
|
||||
SubclassOfInner::Class(class) => class.type_definition(db),
|
||||
SubclassOfInner::TypeVar(bound_typevar) => {
|
||||
Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?))
|
||||
}
|
||||
@@ -6605,7 +6605,7 @@ impl<'db> Type<'db> {
|
||||
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
|
||||
|
||||
Self::ProtocolInstance(protocol) => match protocol.inner {
|
||||
Protocol::FromClass(class) => Some(class.type_definition(db)),
|
||||
Protocol::FromClass(class) => class.type_definition(db),
|
||||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ use crate::{
|
||||
attribute_assignments,
|
||||
definition::{DefinitionKind, TargetKind},
|
||||
place_table,
|
||||
scope::{FileScopeId, ScopeId},
|
||||
scope::ScopeId,
|
||||
semantic_index, use_def_map,
|
||||
},
|
||||
types::{
|
||||
@@ -74,7 +74,7 @@ use ruff_db::diagnostic::Span;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use ruff_python_ast::{self as ast, NodeIndex, PythonVersion};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use ty_module_resolver::{KnownModule, file_to_module};
|
||||
@@ -613,7 +613,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
pub(crate) fn file(self, db: &dyn Db) -> File {
|
||||
match self {
|
||||
Self::Static(class) => class.file(db),
|
||||
Self::Dynamic(class) => class.file(db),
|
||||
Self::Dynamic(class) => class.scope(db).file(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,10 +664,10 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the definition of this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
/// Returns the definition of this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
Self::Static(class) => class.definition(db),
|
||||
Self::Static(class) => Some(class.definition(db)),
|
||||
Self::Dynamic(class) => class.definition(db),
|
||||
}
|
||||
}
|
||||
@@ -675,11 +675,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// Returns the type definition for this class.
|
||||
///
|
||||
/// For static classes, returns `TypeDefinition::StaticClass`.
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass`.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
Self::Static(class) => TypeDefinition::StaticClass(class.definition(db)),
|
||||
Self::Dynamic(class) => TypeDefinition::DynamicClass(class.definition(db)),
|
||||
Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))),
|
||||
Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,13 +944,13 @@ impl<'db> ClassType<'db> {
|
||||
self.class_literal(db).known(db)
|
||||
}
|
||||
|
||||
/// Returns the definition for this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
/// Returns the definition for this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
self.class_literal(db).definition(db)
|
||||
}
|
||||
|
||||
/// Returns the type definition for this class.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
self.class_literal(db).type_definition(db)
|
||||
}
|
||||
|
||||
@@ -4704,12 +4704,8 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
///
|
||||
/// # Salsa interning
|
||||
///
|
||||
/// Each `type()` call is uniquely identified by its [`Definition`], which provides
|
||||
/// stable identity without depending on AST node indices that can change when code
|
||||
/// is inserted above the call site.
|
||||
///
|
||||
/// Two different `type()` calls always produce distinct `DynamicClassLiteral`
|
||||
/// instances, even if they have the same name and bases:
|
||||
/// This is a Salsa-interned struct. Two different `type()` calls always produce
|
||||
/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases:
|
||||
///
|
||||
/// ```python
|
||||
/// Foo1 = type("Foo", (Base,), {})
|
||||
@@ -4717,9 +4713,11 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// # Foo1 and Foo2 are distinct types
|
||||
/// ```
|
||||
///
|
||||
/// Note: Only assigned `type()` calls are currently supported (e.g., `Foo = type(...)`).
|
||||
/// Inline calls like `process(type(...))` fall back to normal call handling.
|
||||
#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)]
|
||||
/// The `anchor` field provides stable identity:
|
||||
/// - For assigned `type()` calls, the `Definition` uniquely identifies the class.
|
||||
/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope
|
||||
/// provides stable identity that only changes when the scope itself changes.
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct DynamicClassLiteral<'db> {
|
||||
/// The name of the class (from the first argument to `type()`).
|
||||
@@ -4730,8 +4728,13 @@ pub struct DynamicClassLiteral<'db> {
|
||||
#[returns(deref)]
|
||||
pub bases: Box<[ClassBase<'db>]>,
|
||||
|
||||
/// The definition where this class is created.
|
||||
pub definition: Definition<'db>,
|
||||
/// The anchor for this dynamic class, providing stable identity.
|
||||
///
|
||||
/// - `Definition`: The `type()` call is assigned to a variable. The definition
|
||||
/// uniquely identifies this class and can be used to find the `type()` call.
|
||||
/// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset
|
||||
/// is relative to the enclosing scope's anchor node index.
|
||||
pub anchor: DynamicClassAnchor<'db>,
|
||||
|
||||
/// The class members from the namespace dict (third argument to `type()`).
|
||||
/// Each entry is a (name, type) pair extracted from the dict literal.
|
||||
@@ -4748,38 +4751,87 @@ pub struct DynamicClassLiteral<'db> {
|
||||
pub dataclass_params: Option<DataclassParams<'db>>,
|
||||
}
|
||||
|
||||
/// Anchor for identifying a dynamic class literal.
|
||||
///
|
||||
/// This enum provides stable identity for `DynamicClassLiteral`:
|
||||
/// - For assigned calls, the `Definition` uniquely identifies the class.
|
||||
/// - For dangling calls, a relative offset provides stable identity.
|
||||
#[derive(
|
||||
Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update, get_size2::GetSize,
|
||||
)]
|
||||
pub enum DynamicClassAnchor<'db> {
|
||||
/// The `type()` call is assigned to a variable.
|
||||
///
|
||||
/// The `Definition` uniquely identifies this class. The `type()` call expression
|
||||
/// is the `value` of the assignment, so we can get its range from the definition.
|
||||
Definition(Definition<'db>),
|
||||
|
||||
/// The `type()` call is "dangling" (not assigned to a variable).
|
||||
///
|
||||
/// The offset is relative to the enclosing scope's anchor node index.
|
||||
/// For module scope, this is equivalent to an absolute index (anchor is 0).
|
||||
ScopeOffset { scope: ScopeId<'db>, offset: u32 },
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for DynamicClassLiteral<'_> {}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> DynamicClassLiteral<'db> {
|
||||
/// Returns the definition where this class is created, if it was assigned to a variable.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self.anchor(db) {
|
||||
DynamicClassAnchor::Definition(definition) => Some(definition),
|
||||
DynamicClassAnchor::ScopeOffset { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the scope in which this dynamic class was created.
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
match self.anchor(db) {
|
||||
DynamicClassAnchor::Definition(definition) => definition.scope(db),
|
||||
DynamicClassAnchor::ScopeOffset { scope, .. } => scope,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Span`] with the range of the `type()` call expression.
|
||||
///
|
||||
/// See [`Self::header_range`] for more details.
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
Span::from(self.file(db)).with_range(self.header_range(db))
|
||||
Span::from(self.scope(db).file(db)).with_range(self.header_range(db))
|
||||
}
|
||||
|
||||
/// Returns the range of the `type()` call expression that created this class.
|
||||
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
|
||||
let definition = self.definition(db);
|
||||
let file = definition.file(db);
|
||||
let scope = self.scope(db);
|
||||
let file = scope.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
|
||||
// Dynamic classes are only created from regular assignments (e.g., `Foo = type(...)`).
|
||||
let DefinitionKind::Assignment(assignment) = definition.kind(db) else {
|
||||
unreachable!("DynamicClassLiteral should only be created from Assignment definitions");
|
||||
};
|
||||
assignment.value(&module).range()
|
||||
}
|
||||
match self.anchor(db) {
|
||||
DynamicClassAnchor::Definition(definition) => {
|
||||
// For definitions, get the range from the definition's value.
|
||||
// The `type()` call is the value of the assignment.
|
||||
definition
|
||||
.kind(db)
|
||||
.value(&module)
|
||||
.expect("DynamicClassAnchor::Definition should only be used for assignments")
|
||||
.range()
|
||||
}
|
||||
DynamicClassAnchor::ScopeOffset { offset, .. } => {
|
||||
// For dangling `type()` calls, compute the absolute index from the offset.
|
||||
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
|
||||
let anchor_u32 = scope_anchor
|
||||
.as_u32()
|
||||
.expect("anchor should not be NodeIndex::NONE");
|
||||
let absolute_index = NodeIndex::from(anchor_u32 + offset);
|
||||
|
||||
/// Returns the file containing the `type()` call.
|
||||
pub(crate) fn file(self, db: &'db dyn Db) -> File {
|
||||
self.definition(db).file(db)
|
||||
}
|
||||
|
||||
/// Returns the scope containing the `type()` call.
|
||||
pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId {
|
||||
self.definition(db).file_scope(db)
|
||||
// Get the node and return its range.
|
||||
let node: &ast::ExprCall = module
|
||||
.get_by_index(absolute_index)
|
||||
.try_into()
|
||||
.expect("scope offset should point to ExprCall");
|
||||
node.range()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metaclass of this dynamic class.
|
||||
@@ -5020,7 +5072,7 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.bases(db),
|
||||
self.definition(db),
|
||||
self.anchor(db),
|
||||
self.members(db),
|
||||
self.has_dynamic_namespace(db),
|
||||
dataclass_params,
|
||||
@@ -5314,7 +5366,8 @@ impl<'db> QualifiedClassName<'db> {
|
||||
}
|
||||
ClassLiteral::Dynamic(class) => {
|
||||
// Dynamic classes don't have a body scope; start from the enclosing scope.
|
||||
(class.file(self.db), class.file_scope(self.db), 0)
|
||||
let scope = class.scope(self.db);
|
||||
(scope.file(self.db), scope.file_scope_id(self.db), 0)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) fn typing_self<'db>(
|
||||
let identity = TypeVarIdentity::new(
|
||||
db,
|
||||
ast::name::Name::new_static("Self"),
|
||||
Some(class.definition(db)),
|
||||
class.definition(db),
|
||||
TypeVarKind::TypingSelf,
|
||||
);
|
||||
let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance(
|
||||
|
||||
@@ -169,9 +169,9 @@ pub fn definitions_for_name<'db>(
|
||||
// instead of `int` (hover only shows the docstring of the first definition).
|
||||
.rev()
|
||||
.filter_map(|ty| ty.as_nominal_instance())
|
||||
.map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db);
|
||||
ResolvedDefinition::Definition(definition)
|
||||
.filter_map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db)?;
|
||||
Some(ResolvedDefinition::Definition(definition))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ use crate::subscript::{PyIndex, PySlice};
|
||||
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
|
||||
use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||
use crate::types::class::{
|
||||
ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
|
||||
MetaclassErrorKind, MethodDecorator,
|
||||
ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral,
|
||||
DynamicMetaclassConflict, FieldKind, MetaclassErrorKind, MethodDecorator,
|
||||
};
|
||||
use crate::types::context::{InNoTypeCheck, InferContext};
|
||||
use crate::types::cyclic::CycleDetector;
|
||||
@@ -5581,7 +5581,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// Try to extract the dynamic class with definition.
|
||||
// This returns `None` if it's not a three-arg call to `type()`,
|
||||
// signalling that we must fall back to normal call inference.
|
||||
self.infer_dynamic_type_expression(call_expr, definition)
|
||||
self.infer_dynamic_type_expression(call_expr, Some(definition))
|
||||
.unwrap_or_else(|| {
|
||||
self.infer_call_expression_impl(call_expr, callable_type, tcx)
|
||||
})
|
||||
@@ -6200,7 +6200,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
fn infer_dynamic_type_expression(
|
||||
&mut self,
|
||||
call_expr: &ast::ExprCall,
|
||||
definition: Definition<'db>,
|
||||
definition: Option<Definition<'db>>,
|
||||
) -> Option<Type<'db>> {
|
||||
let db = self.db();
|
||||
|
||||
@@ -6310,11 +6310,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let (bases, mut disjoint_bases) =
|
||||
self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
|
||||
let scope = self.scope();
|
||||
|
||||
// Create the anchor for identifying this dynamic class.
|
||||
// - For assigned `type()` calls, the Definition uniquely identifies the class.
|
||||
// - For dangling calls, compute a relative offset from the scope's node index.
|
||||
let anchor = if let Some(def) = definition {
|
||||
DynamicClassAnchor::Definition(def)
|
||||
} else {
|
||||
let call_node_index = call_expr.node_index().load();
|
||||
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
|
||||
let anchor_u32 = scope_anchor
|
||||
.as_u32()
|
||||
.expect("scope anchor should not be NodeIndex::NONE");
|
||||
let call_u32 = call_node_index
|
||||
.as_u32()
|
||||
.expect("call node should not be NodeIndex::NONE");
|
||||
DynamicClassAnchor::ScopeOffset {
|
||||
scope,
|
||||
offset: call_u32 - anchor_u32,
|
||||
}
|
||||
};
|
||||
|
||||
let dynamic_class = DynamicClassLiteral::new(
|
||||
db,
|
||||
name,
|
||||
bases,
|
||||
definition,
|
||||
anchor,
|
||||
members,
|
||||
has_dynamic_namespace,
|
||||
None,
|
||||
@@ -9514,6 +9536,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
return Type::TypedDict(typed_dict);
|
||||
}
|
||||
|
||||
// Handle 3-argument `type(name, bases, dict)`.
|
||||
if let Type::ClassLiteral(class) = callable_type
|
||||
&& class.is_known(self.db(), KnownClass::Type)
|
||||
&& let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None)
|
||||
{
|
||||
return dynamic_type;
|
||||
}
|
||||
|
||||
// We don't call `Type::try_call`, because we want to perform type inference on the
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
|
||||
@@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> {
|
||||
|
||||
pub fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.type_definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user