From 597c35458e7b2c8d2e599c899df34c2680d11ba5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Jan 2026 13:40:52 -0500 Subject: [PATCH] Use a tracked struct --- .../resources/mdtest/call/type.md | 45 +++++----- .../resources/mdtest/narrow/type.md | 11 ++- crates/ty_python_semantic/src/ast_node_ref.rs | 2 +- crates/ty_python_semantic/src/types.rs | 8 +- crates/ty_python_semantic/src/types/class.rs | 87 ++++++++++--------- .../ty_python_semantic/src/types/generics.rs | 2 +- .../src/types/ide_support.rs | 6 +- .../src/types/infer/builder.rs | 20 ++++- .../src/types/typed_dict.rs | 4 +- 9 files changed, 102 insertions(+), 83 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 8769f15b8c..df5a21081c 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]] = [] @@ -415,24 +412,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}) ``` @@ -656,20 +651,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 c4c6332ebb..7053a74f35 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -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 ``` diff --git a/crates/ty_python_semantic/src/ast_node_ref.rs b/crates/ty_python_semantic/src/ast_node_ref.rs index a3d1fae49a..da8568db66 100644 --- a/crates/ty_python_semantic/src/ast_node_ref.rs +++ b/crates/ty_python_semantic/src/ast_node_ref.rs @@ -67,7 +67,7 @@ where /// /// This method may panic or produce unspecified results if the provided module is from a /// different file or Salsa revision than the module to which the node belongs. - pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { + pub(crate) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { let index = node.node_index().load(); debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node)); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 76cc4684b1..4c9df1bf3c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6529,9 +6529,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)?)) @@ -6545,7 +6545,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)?)) } @@ -6575,7 +6575,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 ba4f3b61fc..6da7c1bcf7 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -52,6 +52,7 @@ use crate::types::{ }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, + ast_node_ref::AstNodeRef, place::{ Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening, known_module_symbol, place_from_bindings, place_from_declarations, @@ -668,10 +669,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), } } @@ -679,11 +680,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), } } @@ -941,13 +942,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) } @@ -4678,14 +4679,10 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// Supporting namespace dict attributes would require parsing dict literals and tracking /// the attribute types, similar to how TypedDict handles its fields. /// -/// # Salsa interning +/// # Salsa tracking /// -/// 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 tracked struct. Two different `type()` calls always produce +/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: /// /// ```python /// Foo1 = type("Foo", (Base,), {}) @@ -4693,9 +4690,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 `_node_ref` field is marked with `#[tracked]` and `#[no_eq]`, which means +/// it doesn't participate in struct equality comparisons. Instead, accesses to +/// that field are tracked as separate dependencies, providing stable identity +/// across AST changes within the same scope. +#[salsa::tracked(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()`). @@ -4706,8 +4705,26 @@ pub struct DynamicClassLiteral<'db> { #[returns(deref)] pub bases: Box<[ClassBase<'db>]>, - /// The definition where this class is created. - pub definition: Definition<'db>, + /// The file containing the `type()` call. + pub file: File, + + /// The scope containing the `type()` call. + pub file_scope: FileScopeId, + + /// The AST node reference for the `type()` call expression. + /// + /// WARNING: Only access this field when doing type inference for the same + /// file as where `DynamicClassLiteral` is defined to avoid cross-file query dependencies. + #[no_eq] + #[tracked] + #[returns(ref)] + pub(crate) _node_ref: AstNodeRef, + + /// The definition where this class is created (if in an assignment context). + /// + /// This is `Some` when the `type()` call is part of an assignment, + /// allowing go-to-definition to navigate to the creation site. + pub definition: Option>, /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator /// or passed to `dataclass()` as a function. @@ -4727,25 +4744,8 @@ impl<'db> DynamicClassLiteral<'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 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() - } - - /// 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) + let module = parsed_module(db, self.file(db)).load(db); + self._node_ref(db).node(&module).range() } /// Get the metaclass of this dynamic class. @@ -4918,7 +4918,10 @@ impl<'db> DynamicClassLiteral<'db> { Self::new( db, self.name(db).clone(), - self.bases(db), + self.bases(db).into(), + self.file(db), + self.file_scope(db), + self._node_ref(db).clone(), self.definition(db), dataclass_params, ) 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 9abed64628..e1062d5b4e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -24,6 +24,7 @@ use super::{ infer_definition_types, infer_expression_types, infer_same_file_expression_type, infer_unpack_types, }; +use crate::ast_node_ref::AstNodeRef; use crate::diagnostic::format_enumeration; use crate::node_key::NodeKey; use crate::place::{ @@ -5412,7 +5413,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) }) @@ -6031,7 +6032,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(); @@ -6096,7 +6097,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name); - let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None); + let file = self.file(); + let file_scope = self.scope().file_scope_id(db); + let node_ref = AstNodeRef::new(self.module(), call_expr); + let dynamic_class = DynamicClassLiteral::new( + db, name, bases, file, file_scope, node_ref, definition, None, + ); // Check for MRO errors. if let Err(error) = dynamic_class.try_mro(db) { @@ -9079,6 +9085,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, } }