Compare commits
3 Commits
main
...
charlie/dy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2d277ba3 | ||
|
|
e7f60c28a3 | ||
|
|
6e9baff4d9 |
@@ -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]] = []
|
||||
@@ -501,37 +498,34 @@ Other numbers of arguments are invalid:
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Foo", ())) # revealed: Unknown
|
||||
|
||||
# TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
|
||||
# The keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
|
||||
# (you cannot pass `metaclass=` to `type()`, and none of them have
|
||||
# base classes with `__init_subclass__` methods),
|
||||
# but `type[Unknown]` would be better than `Unknown` here
|
||||
#
|
||||
# base classes with `__init_subclass__` methods).
|
||||
# We return `type[Unknown]` to reduce false positives from attribute access.
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: type[Unknown]
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: type[Unknown]
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: type[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 +592,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:
|
||||
@@ -851,46 +858,51 @@ def f(*args, **kwargs):
|
||||
A = type(*args, **kwargs)
|
||||
reveal_type(A) # revealed: type[Unknown]
|
||||
|
||||
# Has a string first arg, but unknown additional args from *args
|
||||
# Has a string first arg, but unknown additional args from *args.
|
||||
# We return type[Unknown] since the overload is ambiguous.
|
||||
B = type("B", *args, **kwargs)
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(B) # revealed: <class 'str'>
|
||||
reveal_type(B) # revealed: type[Unknown]
|
||||
|
||||
# Has string and tuple, but unknown additional args
|
||||
# Has string and tuple, but unknown additional args from *args.
|
||||
C = type("C", (), *args, **kwargs)
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(C) # revealed: type
|
||||
reveal_type(C) # revealed: type[Unknown]
|
||||
|
||||
# All three positional args provided, only **kwargs unknown
|
||||
# All three positional args provided, only **kwargs unknown.
|
||||
D = type("D", (), {}, **kwargs)
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(D) # revealed: type
|
||||
reveal_type(D) # revealed: type[Unknown]
|
||||
|
||||
# Three starred expressions - we can't know how they expand
|
||||
a = ("E",)
|
||||
b = ((),)
|
||||
c = ({},)
|
||||
E = type(*a, *b, *c)
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(E) # revealed: type
|
||||
reveal_type(E) # revealed: type[Unknown]
|
||||
|
||||
# Non-string first argument with **kwargs - still ambiguous, return type[Unknown]
|
||||
F = type(123, **kwargs)
|
||||
reveal_type(F) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -169,12 +169,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)]
|
||||
|
||||
@@ -6536,9 +6536,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)?))
|
||||
@@ -6552,7 +6552,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)?))
|
||||
}
|
||||
@@ -6582,7 +6582,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,10 +672,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),
|
||||
}
|
||||
}
|
||||
@@ -683,11 +683,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,13 +952,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)
|
||||
}
|
||||
|
||||
@@ -4707,12 +4707,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,), {})
|
||||
@@ -4720,9 +4716,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()`).
|
||||
@@ -4733,8 +4731,16 @@ pub struct DynamicClassLiteral<'db> {
|
||||
#[returns(deref)]
|
||||
pub bases: Box<[ClassBase<'db>]>,
|
||||
|
||||
/// The definition where this class is created.
|
||||
pub definition: Definition<'db>,
|
||||
/// The scope containing the `type()` call.
|
||||
pub scope: ScopeId<'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.
|
||||
@@ -4751,38 +4757,79 @@ 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(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 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.
|
||||
@@ -5023,7 +5070,8 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.bases(db),
|
||||
self.definition(db),
|
||||
self.scope(db),
|
||||
self.anchor(db),
|
||||
self.members(db),
|
||||
self.has_dynamic_namespace(db),
|
||||
dataclass_params,
|
||||
@@ -5317,7 +5365,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::{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;
|
||||
@@ -69,11 +69,11 @@ use crate::types::diagnostic::{
|
||||
INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
|
||||
INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
|
||||
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
|
||||
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
|
||||
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
|
||||
UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
|
||||
hint_if_stdlib_attribute_exists_on_other_versions,
|
||||
NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
|
||||
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
|
||||
TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
|
||||
USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
|
||||
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
|
||||
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
|
||||
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
|
||||
@@ -5448,9 +5448,9 @@ 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)
|
||||
self.infer_type_call_fallback(call_expr, callable_type, tcx)
|
||||
})
|
||||
}
|
||||
Some(_) | None => {
|
||||
@@ -6056,6 +6056,67 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback for `type()` calls when `infer_dynamic_type_expression` returns `None`.
|
||||
///
|
||||
/// This handles ambiguous `type()` calls that have variadic arguments (`*args`, `**kwargs`)
|
||||
/// or invalid keyword arguments. In these cases, we return `type[Unknown]` instead of
|
||||
/// going through normal overload resolution, which could give misleading results
|
||||
/// (e.g., matching the single-argument overload when there might be more arguments).
|
||||
fn infer_type_call_fallback(
|
||||
&mut self,
|
||||
call_expr: &ast::ExprCall,
|
||||
callable_type: Type<'db>,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Type<'db> {
|
||||
self.try_type_call_fallback(call_expr)
|
||||
.unwrap_or_else(|| self.infer_call_expression_impl(call_expr, callable_type, tcx))
|
||||
}
|
||||
|
||||
/// Try to handle an ambiguous `type()` call with variadic arguments or invalid kwargs.
|
||||
///
|
||||
/// Returns `Some(type[Unknown])` if this is an ambiguous `type()` call that should
|
||||
/// not go through normal overload resolution. Returns `None` if normal call
|
||||
/// inference should proceed.
|
||||
fn try_type_call_fallback(&mut self, call_expr: &ast::ExprCall) -> Option<Type<'db>> {
|
||||
let arguments = &call_expr.arguments;
|
||||
|
||||
// Check for variadic arguments that make the overload ambiguous.
|
||||
let has_starred_args = arguments.args.iter().any(ast::Expr::is_starred_expr);
|
||||
let has_kwargs_unpack = arguments.keywords.iter().any(|kw| kw.arg.is_none());
|
||||
|
||||
// If we have variadic arguments, we can't determine which overload of `type()`
|
||||
// is being called. Return type[Unknown].
|
||||
if has_starred_args || has_kwargs_unpack {
|
||||
for arg in &arguments.args {
|
||||
self.infer_expression(arg, TypeContext::default());
|
||||
}
|
||||
for kw in &arguments.keywords {
|
||||
self.infer_expression(&kw.value, TypeContext::default());
|
||||
}
|
||||
return Some(SubclassOfType::subclass_of_unknown());
|
||||
}
|
||||
|
||||
// If we have explicit keyword arguments (not **kwargs unpack), this is an invalid
|
||||
// type() call. Return type[Unknown] instead of Unknown to reduce false positives.
|
||||
if !arguments.keywords.is_empty() && !has_kwargs_unpack && arguments.args.len() == 3 {
|
||||
// Infer all argument types to ensure they're stored.
|
||||
for arg in &arguments.args {
|
||||
self.infer_expression(arg, TypeContext::default());
|
||||
}
|
||||
for kw in &arguments.keywords {
|
||||
self.infer_expression(&kw.value, TypeContext::default());
|
||||
}
|
||||
|
||||
if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) {
|
||||
builder.into_diagnostic("No overload of class `type` matches arguments");
|
||||
}
|
||||
|
||||
return Some(SubclassOfType::subclass_of_unknown());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Try to infer a 3-argument `type(name, bases, dict)` call expression, capturing the definition.
|
||||
///
|
||||
/// This is called when we detect a `type()` call in assignment context and want to
|
||||
@@ -6067,7 +6128,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();
|
||||
|
||||
@@ -6177,11 +6238,31 @@ 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(call_u32 - anchor_u32)
|
||||
};
|
||||
|
||||
let dynamic_class = DynamicClassLiteral::new(
|
||||
db,
|
||||
name,
|
||||
bases,
|
||||
definition,
|
||||
scope,
|
||||
anchor,
|
||||
members,
|
||||
has_dynamic_namespace,
|
||||
None,
|
||||
@@ -9293,6 +9374,21 @@ 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)
|
||||
{
|
||||
if let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None) {
|
||||
return dynamic_type;
|
||||
}
|
||||
|
||||
// Fallback for ambiguous `type()` calls (with `*args`, `**kwargs`, or invalid kwargs).
|
||||
let result = self.try_type_call_fallback(call_expression);
|
||||
if let Some(ty) = result {
|
||||
return ty;
|
||||
}
|
||||
}
|
||||
|
||||
// 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