diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 5f0f7dd844..a23055ccdb 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1649,6 +1649,65 @@ TracebackType 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 = DynClass() +"#, + ) + .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("Bar", (), {})): + pass +"#, + ) + .build(); + + assert_snapshot!(test.goto_definition(), @"No goto target found"); + } + // TODO: Should only list `a: int` #[test] fn redeclarations() { diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 609fff6c4d..4a29a10e7b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -20,16 +20,13 @@ class Base: ... class Mixin: ... # We synthesize a class type using the name argument -Foo = type("Foo", (), {}) -reveal_type(Foo) # revealed: +reveal_type(type("Foo", (), {})) # revealed: # With a single base class -Foo2 = type("Foo", (Base,), {"attr": 1}) -reveal_type(Foo2) # revealed: +reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: # With multiple base classes -Foo3 = type("Foo", (Base, Mixin), {}) -reveal_type(Foo3) # revealed: +reveal_type(type("Foo", (Base, Mixin), {})) # revealed: # 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 ``" +# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found ``" 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) + + + +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: + +# The annotation `type[Base]` is compatible with the inferred type class Base: ... -# TODO: Should infer `` instead of `type` -T: type = type("T", (), {}) -reveal_type(T) # revealed: type - -# TODO: Should infer `` 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: + +# Incompatible annotation produces an error +class Unrelated: ... + +# error: [invalid-assignment] +Bad: type[Unrelated] = type("Bad", (Base,), {}) ``` ## Special base classes diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 68db43a4c8..2a68069abe 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/type.md_-_Calls_to_`type()`_-_MRO_error_highlighti…_(12acd974e75461ea).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/type.md_-_Calls_to_`type()`_-_MRO_error_highlighti…_(12acd974e75461ea).snap new file mode 100644 index 0000000000..67d47b6f7c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/type.md_-_Calls_to_`type()`_-_MRO_error_highlighti…_(12acd974e75461ea).snap @@ -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 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 + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index d156c3ad1d..e0c7cf8274 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -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)] diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index c7c42241a3..2613cccfed 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -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 { + 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)] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 73f1a50b87..f206765569 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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, }, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 1f30a0ddca..d23e4c219c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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> { 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> { 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> { 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> { 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>, } +/// 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> { + 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) } }; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index b467f08bbe..290daac22a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -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( diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 70fa611c77..73497948b1 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -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(); } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 242cd542cf..945ee83b0f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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>, ) -> Option> { 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. diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index d7a93d8bcf..66543694ad 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> { pub fn definition(self, db: &'db dyn Db) -> Option> { 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> { match self { - TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)), + TypedDictType::Class(defining_class) => defining_class.type_definition(db), TypedDictType::Synthesized(_) => None, } }