diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 42602a23272823..4f4a9d052d51e2 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -127,6 +127,127 @@ reveal_type(alice5.id) # revealed: int reveal_type(alice5.name) # revealed: str ``` +### Functional syntax with string annotations + +String annotations (forward references) are properly evaluated to types: + +```py +from typing import NamedTuple + +Point = NamedTuple("Point", [("x", "int"), ("y", "int")]) +p = Point(1, 2) + +reveal_type(p.x) # revealed: int +reveal_type(p.y) # revealed: int +``` + +Recursive references in functional syntax are supported: + +```py +from typing import NamedTuple + +Node = NamedTuple("Node", [("value", int), ("next", "Node | None")]) +n = Node(1, None) + +reveal_type(n.value) # revealed: int +reveal_type(n.next) # revealed: Node | None +``` + +### Functional syntax with tuple literal + +Using a tuple literal for fields instead of a list: + +```py +from typing import NamedTuple + +Point = NamedTuple("Point", (("x", int), ("y", int))) +p = Point(1, 2) + +reveal_type(p.x) # revealed: int +reveal_type(p.y) # revealed: int +``` + +### Functional syntax with variable fields and string annotations + +String annotations in variable fields don't currently resolve (this is a known limitation). The +types are extracted from the inferred tuple type, but string literals don't get evaluated as type +expressions: + +```py +from typing import NamedTuple + +fields = [("value", "int"), ("label", "str")] +Item = NamedTuple("Item", fields) +item = Item(42, "test") + +# TODO: These should resolve to `int` and `str`, but string annotations in variable fields +# aren't currently evaluated. +reveal_type(item.value) # revealed: Any +reveal_type(item.label) # revealed: Any +``` + +When using non-string types in variable fields, they work correctly when using a tuple literal: + +```py +from typing import NamedTuple + +tree_fields = (("value", int), ("left", int | None), ("right", int | None)) +Tree = NamedTuple("Tree", tree_fields) +t = Tree(1, None, None) + +reveal_type(t.value) # revealed: int +reveal_type(t.left) # revealed: int | None +reveal_type(t.right) # revealed: int | None +``` + +### Functional syntax as base class (dangling call) + +When NamedTuple is used directly as a base class without being assigned to a variable first, it's a +"dangling call". The types are still properly inferred: + +```py +from typing import NamedTuple + +class Point(NamedTuple("Point", [("x", int), ("y", int)])): + def magnitude(self) -> float: + return (self.x**2 + self.y**2) ** 0.5 + +p = Point(3, 4) +reveal_type(p.x) # revealed: int +reveal_type(p.y) # revealed: int +reveal_type(p.magnitude()) # revealed: int | float +``` + +String annotations in dangling calls work correctly for forward references to classes defined in the +same scope. This allows recursive types: + +```py +from typing import NamedTuple + +class Node(NamedTuple("Node", [("value", int), ("next", "Node | None")])): + pass + +n = Node(1, None) +reveal_type(n.value) # revealed: int +reveal_type(n.next) # revealed: Node | None +``` + +Note that the string annotation must reference a name that exists in scope. References to the +internal NamedTuple name (if different from the class name) won't work: + +```py +from typing import NamedTuple + +# The string "X" refers to the internal name, not "BadNode", so it won't resolve: +class BadNode(NamedTuple("X", [("value", int), ("next", "X | None")])): + pass + +n = BadNode(1, None) +reveal_type(n.value) # revealed: int +# X is not in scope, so it resolves to Unknown; None is correctly resolved +reveal_type(n.next) # revealed: Unknown | None +``` + ### Functional syntax with variable name When the typename is passed via a variable, we can extract it from the inferred literal string type: @@ -196,6 +317,25 @@ reveal_type(url[1]) # revealed: int url[2] ``` +### Functional syntax with Final variable field names + +When field names are `Final` variables, they resolve to their literal string values: + +```py +from typing import Final, NamedTuple + +X: Final = "x" +Y: Final = "y" +N = NamedTuple("N", [(X, int), (Y, int)]) + +reveal_type(N(x=3, y=4).x) # revealed: int +reveal_type(N(x=3, y=4).y) # revealed: int + +# error: [invalid-argument-type] +# error: [invalid-argument-type] +N(x="", y="") +``` + ### Functional syntax with variadic tuple fields When fields are passed as a variadic tuple (e.g., `tuple[..., *tuple[T, ...]]`), we cannot determine @@ -887,6 +1027,8 @@ reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float ``` +### Functional syntax with generics + Generic namedtuples can also be defined using the functional syntax with type variables in the field types. We don't currently support this, but mypy does: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0cafb153be3144..6e0e912025532e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -30,8 +30,9 @@ pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; pub(crate) use self::diagnostic::register_lints; pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; pub(crate) use self::infer::{ - TypeContext, infer_deferred_types, infer_definition_types, infer_expression_type, - infer_expression_types, infer_scope_types, static_expression_truthiness, + DeferredAnchor, TypeContext, infer_deferred_namedtuple_call_types, infer_deferred_types, + infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, + static_expression_truthiness, }; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::{CallableSignature, Signature}; diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 71e1a6edecaf3e..cda4c9529665a3 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3,11 +3,13 @@ use std::cell::RefCell; use std::fmt::Write; use std::sync::{LazyLock, Mutex}; -use super::TypeVarVariance; use super::{ BoundTypeVarInstance, MemberLookupPolicy, Mro, MroIterator, SpecialFormType, StaticMroError, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, - function::FunctionType, + function::FunctionType, infer_scope_types, +}; +use super::{ + DeferredAnchor, TypeVarVariance, infer_deferred_namedtuple_call_types, infer_deferred_types, }; use crate::place::{DefinedPlace, TypeOrigin}; use crate::semantic_index::definition::{Definition, DefinitionState}; @@ -172,6 +174,45 @@ fn fields_cycle_initial<'db>( FxIndexMap::default() } +/// Cycle initial function for `DynamicNamedTupleLiteral::mro`. +/// +/// Calls `tuple_base_class()` which may call `fields()`. If `fields()` is also +/// in the cycle, its `cycle_initial` will return `Unknown` types, breaking the cycle. +fn dynamic_namedtuple_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicNamedTupleLiteral<'db>, +) -> Mro<'db> { + let self_base = ClassBase::Class(ClassType::NonGeneric(self_.into())); + let tuple_class = self_.tuple_base_class(db); + let object_class = KnownClass::Object + .to_class_literal(db) + .as_class_literal() + .expect("object should be a class literal") + .default_specialization(db); + Mro::from([ + self_base, + ClassBase::Class(tuple_class), + ClassBase::Class(object_class), + ]) +} + +/// Cycle initial function for `DynamicNamedTupleLiteral::fields`. +/// +/// Returns fields with `Unknown` types to break cycles in recursive namedtuples. +fn dynamic_namedtuple_fields_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicNamedTupleLiteral<'db>, +) -> Box<[(Name, Type<'db>, Option>)]> { + self_ + .raw_fields(db) + .iter() + .cloned() + .map(|name| (name, Type::unknown(), None)) + .collect() +} + /// A category of classes with code generation capabilities (with synthesized methods). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum CodeGeneratorKind<'db> { @@ -4814,7 +4855,7 @@ pub struct DynamicClassLiteral<'db> { /// 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>, + pub anchor: DeferredAnchor<'db>, /// The class members from the namespace dict (third argument to `type()`). /// Each entry is a (name, type) pair extracted from the dict literal. @@ -4831,28 +4872,6 @@ 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] @@ -4860,16 +4879,16 @@ 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, + DeferredAnchor::Definition(definition) => Some(definition), + DeferredAnchor::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, + DeferredAnchor::Definition(definition) => definition.scope(db), + DeferredAnchor::ScopeOffset { scope, .. } => scope, } } @@ -4887,16 +4906,16 @@ impl<'db> DynamicClassLiteral<'db> { let module = parsed_module(db, file).load(db); match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { + DeferredAnchor::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") + .expect("DeferredAnchor::Definition should only be used for assignments") .range() } - DynamicClassAnchor::ScopeOffset { offset, .. } => { + DeferredAnchor::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 @@ -5279,6 +5298,41 @@ fn synthesize_namedtuple_class_member<'db>( } } +/// Extract field types from the inferred type of the fields argument. +/// +/// This handles cases where field specs come from variables rather than AST literals. +fn extract_field_types_from_inferred<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + fields_type: Type<'db>, +) -> FxIndexMap<&'db str, Type<'db>> { + let mut field_types = FxIndexMap::default(); + + let Some(tuple_spec) = fields_type.tuple_instance_spec(db) else { + return field_types; + }; + let Some(fixed_tuple) = tuple_spec.as_fixed_length() else { + return field_types; + }; + + for field_tuple in fixed_tuple.all_elements() { + // Each field must be a fixed-length tuple of exactly 2 elements. + if let Some(field_spec) = field_tuple.exact_tuple_instance_spec(db) + && let Some(field_fixed) = field_spec.as_fixed_length() + && let [Type::StringLiteral(name_lit), field_type] = field_fixed.all_elements() + { + let field_name = name_lit.value(db); + // Convert value type to type expression. + let field_ty = field_type + .in_type_expression(db, scope, None) + .unwrap_or_else(|error| error.fallback_type); + field_types.insert(field_name, field_ty); + } + } + + field_types +} + /// A namedtuple created via the functional form `namedtuple(name, fields)` or /// `NamedTuple(name, fields)`. /// @@ -5299,12 +5353,27 @@ pub struct DynamicNamedTupleLiteral<'db> { #[returns(ref)] pub name: Name, - /// The fields as (name, type, default) tuples. + /// The field names for this namedtuple. + /// + /// For `typing.NamedTuple`, field types are computed lazily in `fields()` + /// by re-reading the AST and using deferred inference. This supports + /// recursive namedtuples where field types may reference the namedtuple being defined. + /// /// For `collections.namedtuple`, all types are `Any`. - /// For `typing.NamedTuple`, types come from the field definitions. - /// The third element is the default type, if any. #[returns(ref)] - pub fields: Box<[(Name, Type<'db>, Option>)]>, + pub raw_fields: Box<[Name]>, + + /// The kind of namedtuple (`typing.NamedTuple` or `collections.namedtuple`). + pub kind: NamedTupleKind, + + /// The default value types for fields with defaults. + /// + /// For `collections.namedtuple`, these come from the `defaults` parameter. + /// For `typing.NamedTuple` (functional form), defaults are not supported. + /// The defaults apply to the rightmost fields (i.e., the last N fields + /// where N is the length of this slice). + #[returns(ref)] + pub default_types: Box<[Type<'db>]>, /// Whether the fields are known statically. /// @@ -5319,7 +5388,7 @@ pub struct DynamicNamedTupleLiteral<'db> { /// uniquely identifies this namedtuple and can be used to find the call. /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset /// is relative to the enclosing scope's anchor node index. - pub anchor: DynamicClassAnchor<'db>, + pub anchor: DeferredAnchor<'db>, } impl get_size2::GetSize for DynamicNamedTupleLiteral<'_> {} @@ -5329,16 +5398,16 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// Returns the definition where this namedtuple 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, + DeferredAnchor::Definition(definition) => Some(definition), + DeferredAnchor::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, + DeferredAnchor::Definition(definition) => definition.scope(db), + DeferredAnchor::ScopeOffset { scope, .. } => scope, } } @@ -5354,16 +5423,16 @@ impl<'db> DynamicNamedTupleLiteral<'db> { let module = parsed_module(db, file).load(db); match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { + DeferredAnchor::Definition(definition) => { // For definitions, get the range from the definition's value. // The namedtuple call is the value of the assignment. definition .kind(db) .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") + .expect("DeferredAnchor::Definition should only be used for assignments") .range() } - DynamicClassAnchor::ScopeOffset { offset, .. } => { + DeferredAnchor::ScopeOffset { offset, .. } => { // For dangling 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 @@ -5386,12 +5455,189 @@ impl<'db> DynamicNamedTupleLiteral<'db> { Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) } + /// Compute field types for this namedtuple. + /// + /// For `typing.NamedTuple`, field types are computed lazily using deferred inference. + /// For `collections.namedtuple`, all field types are `Any`. + /// + /// Returns `(name, type, default_type)` tuples where `default_type` is `Some` for + /// fields with defaults (only for `collections.namedtuple` via the `defaults` parameter). + #[salsa::tracked( + returns(ref), + cycle_initial = dynamic_namedtuple_fields_cycle_initial, + heap_size = ruff_memory_usage::heap_size + )] + pub(crate) fn fields(self, db: &'db dyn Db) -> Box<[(Name, Type<'db>, Option>)]> { + let raw_fields = self.raw_fields(db); + let kind = self.kind(db); + let default_types = self.default_types(db); + let num_defaults = default_types.len(); + let num_fields = raw_fields.len(); + + // For `collections.namedtuple`, all types are `Any`. + if kind == NamedTupleKind::Collections { + return raw_fields + .iter() + .enumerate() + .map(|(i, name)| { + let default_ty = if num_defaults > 0 && i >= num_fields - num_defaults { + // Get the actual default type from the stored defaults. + let default_index = i - (num_fields - num_defaults); + Some(default_types[default_index]) + } else { + None + }; + (name.clone(), Type::any(), default_ty) + }) + .collect(); + } + + // For `typing.NamedTuple`, compute field types. + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + // Get the call expression, either from the definition or from the scope offset. + let (call_expr, scope_offset): (&ast::ExprCall, Option) = match self.anchor(db) { + DeferredAnchor::Definition(definition) => { + let expr = definition + .kind(db) + .value(&module) + .expect("NamedTuple definition should have a value") + .as_call_expr() + .expect("NamedTuple definition value should be a Call"); + (expr, None) + } + DeferredAnchor::ScopeOffset { 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); + let expr = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + (expr, Some(offset)) + } + }; + + // The second positional argument should be the fields list/tuple. + let Some(fields_arg) = call_expr.arguments.args.get(1) else { + return raw_fields + .iter() + .cloned() + .map(|name| (name, Type::unknown(), None)) + .collect(); + }; + + // Try to get field type expressions from AST literal. + // If the fields argument is not a literal (e.g., a variable), we'll fall back to + // extracting types from the inferred type. + let elements: Option<&[ast::Expr]> = match fields_arg { + ast::Expr::List(list) => Some(&list.elts), + ast::Expr::Tuple(tuple) => Some(&tuple.elts), + _ => None, + }; + + // We need scope inference to resolve field names from variables (e.g., Final variables). + let scope_inference = infer_scope_types(db, scope); + + // Build a mapping from field names to their type expressions (for AST literals). + // Field names may be string literals or variables (e.g., Final variables). + let mut field_type_exprs = FxIndexMap::<&str, &ast::Expr>::default(); + if let Some(elements) = elements { + for elt in elements { + // Each element should be a tuple like ("field_name", type). + let field_spec_elts: &[ast::Expr] = match elt { + ast::Expr::Tuple(tuple) => &tuple.elts, + ast::Expr::List(list) => &list.elts, + _ => continue, + }; + let [field_name_expr, field_type_expr] = &field_spec_elts else { + continue; + }; + + // Resolve the field name via inference to handle Final variables. + let field_name_ty = scope_inference.expression_type(field_name_expr); + if let Some(field_name_lit) = field_name_ty.as_string_literal() { + let field_name = field_name_lit.value(db); + field_type_exprs.insert(field_name, field_type_expr); + } + } + } + + // Build a mapping from field names to their types (from the inferred type). + // This is used as a fallback when field specs come from variables. + let field_types_from_inferred = extract_field_types_from_inferred( + db, + scope, + scope_inference.expression_type(fields_arg), + ); + + // Get the definition for deferred inference (if this is a definition-based anchor). + let definition = match self.anchor(db) { + DeferredAnchor::Definition(def) => Some(def), + DeferredAnchor::ScopeOffset { .. } => None, + }; + + // Resolve each field's type, preferring deferred inference when available. + // `typing.NamedTuple` functional form doesn't support defaults. + raw_fields + .iter() + .cloned() + .map(|name| { + // Try deferred inference first for fields with AST type expressions. + let field_ty = if let Some(type_expr) = field_type_exprs.get(name.as_str()) { + let deferred_ty = match (definition, scope_offset) { + (Some(def), _) => { + infer_deferred_types(db, def).try_expression_type(*type_expr) + } + (None, Some(offset)) => { + infer_deferred_namedtuple_call_types(db, scope, offset) + .try_expression_type(*type_expr) + } + _ => None, + }; + + if let Some(ty) = deferred_ty { + // For TypeVar instances, bind them to the definition. + if let Type::KnownInstance(KnownInstanceType::TypeVar(_)) = ty { + ty.in_type_expression(db, scope, definition) + .unwrap_or_else(|error| error.fallback_type) + } else { + ty + } + } else { + // Fall back to inferred type from variable. + field_types_from_inferred + .get(name.as_str()) + .copied() + .unwrap_or_else(Type::unknown) + } + } else { + // No AST type expression; use inferred type from variable. + field_types_from_inferred + .get(name.as_str()) + .copied() + .unwrap_or_else(Type::unknown) + }; + + (name, field_ty, None) + }) + .collect() + } + /// Compute the MRO for this namedtuple. /// /// The MRO is `[self, tuple[field_types...], object]`. /// For example, `namedtuple("Point", [("x", int), ("y", int)])` has MRO /// `[Point, tuple[int, int], object]`. - #[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)] + #[salsa::tracked( + returns(ref), + cycle_initial = dynamic_namedtuple_mro_cycle_initial, + heap_size = ruff_memory_usage::heap_size + )] pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); let tuple_class = self.tuple_base_class(db); @@ -8035,3 +8281,43 @@ mod tests { } } } + +/// The kind of namedtuple: `typing.NamedTuple` or `collections.namedtuple`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)] +pub enum NamedTupleKind { + /// `collections.namedtuple` - all field types are `Any`. + Collections, + /// `typing.NamedTuple` - field types come from type annotations. + Typing, +} + +impl get_size2::GetSize for NamedTupleKind {} + +impl NamedTupleKind { + pub(crate) const fn is_collections(self) -> bool { + matches!(self, Self::Collections) + } + + pub(crate) const fn is_typing(self) -> bool { + matches!(self, Self::Typing) + } + + pub(crate) fn from_type(db: &dyn Db, ty: Type) -> Option { + match ty { + Type::SpecialForm(SpecialFormType::NamedTuple) => Some(NamedTupleKind::Typing), + Type::FunctionLiteral(function) => function + .is_known(db, KnownFunction::NamedTuple) + .then_some(NamedTupleKind::Collections), + _ => None, + } + } +} + +impl std::fmt::Display for NamedTupleKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + NamedTupleKind::Collections => "namedtuple", + NamedTupleKind::Typing => "NamedTuple", + }) + } +} diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f437f128e581d3..e3a456ad2d2f6d 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -160,8 +160,13 @@ pub(crate) fn infer_deferred_types<'db>( let index = semantic_index(db, file); - TypeInferenceBuilder::new(db, InferenceRegion::Deferred(definition), index, &module) - .finish_definition() + TypeInferenceBuilder::new( + db, + InferenceRegion::Deferred(DeferredAnchor::Definition(definition)), + index, + &module, + ) + .finish_definition() } fn deferred_cycle_recovery<'db>( @@ -182,6 +187,37 @@ fn deferred_cycle_initial<'db>( DefinitionInference::cycle_initial(definition.scope(db), Type::divergent(id)) } +/// Infer deferred field types for a dangling `NamedTuple` call. +/// +/// This is used when a `NamedTuple` call is used directly as a base class without being +/// assigned to a variable first. The offset is relative to the scope's anchor node index. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +pub(crate) fn infer_deferred_namedtuple_call_types<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + offset: u32, +) -> ExpressionInference<'db> { + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + let _span = tracing::trace_span!( + "infer_deferred_namedtuple_call_types", + scope = ?scope.as_id(), + offset, + ?file + ) + .entered(); + + let index = semantic_index(db, file); + + TypeInferenceBuilder::new( + db, + InferenceRegion::Deferred(DeferredAnchor::ScopeOffset { scope, offset }), + index, + &module, + ) + .finish_expression() +} + /// Infer all types for an [`Expression`] (including sub-expressions). /// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an /// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a @@ -503,16 +539,58 @@ pub(crate) fn nearest_enclosing_function<'db>( }) } +/// Identifies a region for deferred type inference. +/// +/// Deferred inference is used when type expressions contain forward references +/// (string annotations) that need to be resolved after the full scope is available. +/// +/// There are two cases: +/// - For definitions (functions, classes, assignments), the `Definition` uniquely +/// identifies the region containing annotations that need deferred resolution. +/// - For dangling calls (e.g., `NamedTuple(...)` used directly as a base class), +/// we store a relative offset from the enclosing scope's anchor node index. +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update, get_size2::GetSize, +)] +pub enum DeferredAnchor<'db> { + /// A definition containing annotations that need deferred resolution. + /// + /// This covers functions (parameter/return annotations), classes (base classes), + /// type variables, and assignments (including `NamedTuple(...)` assignments). + Definition(Definition<'db>), + + /// A dangling call expression (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). + /// This is used for patterns like `class Foo(NamedTuple("Foo", [...]))`. + ScopeOffset { scope: ScopeId<'db>, offset: u32 }, +} + +impl<'db> DeferredAnchor<'db> { + /// Returns the scope containing this deferred region. + pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { + match self { + DeferredAnchor::Definition(definition) => definition.scope(db), + DeferredAnchor::ScopeOffset { scope, .. } => scope, + } + } +} + /// A region within which we can infer types. #[derive(Copy, Clone, Debug)] pub(crate) enum InferenceRegion<'db> { - /// infer types for a standalone [`Expression`] + /// Infer types for a standalone [`Expression`] Expression(Expression<'db>, TypeContext<'db>), - /// infer types for a [`Definition`] + /// Infer types for a [`Definition`] Definition(Definition<'db>), - /// infer deferred types for a [`Definition`] - Deferred(Definition<'db>), - /// infer types for an entire [`ScopeId`] + /// Infer deferred types (forward references / string annotations). + /// + /// The anchor identifies the region - either by its definition (for functions, + /// classes, assignments) or by scope offset (for dangling calls like + /// `NamedTuple(...)` used directly as base classes). + Deferred(DeferredAnchor<'db>), + /// Infer types for an entire [`ScopeId`] Scope(ScopeId<'db>), } @@ -520,9 +598,8 @@ impl<'db> InferenceRegion<'db> { fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { match self { InferenceRegion::Expression(expression, _) => expression.scope(db), - InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { - definition.scope(db) - } + InferenceRegion::Definition(definition) => definition.scope(db), + InferenceRegion::Deferred(anchor) => anchor.scope(db), InferenceRegion::Scope(scope) => scope, } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1aa6555b3e95fb..29c574d11f552d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -22,6 +22,7 @@ use ty_module_resolver::{ resolve_module, search_paths, }; +use super::DeferredAnchor; use super::{ DefinitionInference, DefinitionInferenceExtra, ExpressionInference, ExpressionInferenceExtra, InferenceRegion, ScopeInference, ScopeInferenceExtra, infer_deferred_types, @@ -57,11 +58,11 @@ use crate::semantic_index::{ 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::DynamicNamedTupleLiteral; use crate::types::class::{ - ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, - DynamicMetaclassConflict, FieldKind, MetaclassErrorKind, MethodDecorator, + ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind, + MetaclassErrorKind, MethodDecorator, }; +use crate::types::class::{DynamicNamedTupleLiteral, NamedTupleKind}; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ @@ -536,7 +537,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match self.region { InferenceRegion::Scope(scope) => self.infer_region_scope(scope), InferenceRegion::Definition(definition) => self.infer_region_definition(definition), - InferenceRegion::Deferred(definition) => self.infer_region_deferred(definition), + InferenceRegion::Deferred(anchor) => self.infer_region_deferred(anchor), InferenceRegion::Expression(expression, tcx) => { self.infer_region_expression(expression, tcx); } @@ -1653,7 +1654,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - fn infer_region_deferred(&mut self, definition: Definition<'db>) { + fn infer_region_deferred(&mut self, anchor: DeferredAnchor<'db>) { + match anchor { + DeferredAnchor::Definition(definition) => { + self.infer_region_deferred_definition(definition); + } + DeferredAnchor::ScopeOffset { scope, offset } => { + self.infer_region_deferred_scope_offset(scope, offset); + } + } + } + + fn infer_region_deferred_definition(&mut self, definition: Definition<'db>) { // N.B. We don't defer the types for an annotated assignment here because it is done in // the same definition query. It utilizes the deferred expression state instead. // @@ -1683,6 +1695,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer deferred field types for a dangling call (e.g., `NamedTuple`, `TypedDict`). + /// + /// This looks up the call expression using the scope offset and infers the field type + /// annotations with deferred state, enabling forward references. + fn infer_region_deferred_scope_offset(&mut self, scope: ScopeId<'db>, offset: u32) { + let db = self.db(); + + // Compute the absolute node index from the scope anchor + offset. + 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 absolute_index = NodeIndex::from(anchor_u32 + offset); + + // Look up the call expression from the parsed module. + let call_expr: &ast::ExprCall = self + .module() + .get_by_index(absolute_index) + .try_into() + .expect("offset should point to ExprCall"); + + // Infer the field type annotations with deferred state. + self.infer_functional_namedtuple_deferred(&call_expr.arguments); + } + fn infer_region_expression(&mut self, expression: Expression<'db>, tcx: TypeContext<'db>) { match expression.kind(self.db()) { ExpressionKind::Normal => { @@ -6158,6 +6195,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_newtype_assignment_deferred(arguments); return; } + if matches!(func_ty, Type::SpecialForm(SpecialFormType::NamedTuple)) { + self.infer_functional_namedtuple_deferred(arguments); + return; + } for arg in arguments.args.iter().skip(1) { self.infer_type_expression(arg); } @@ -6414,7 +6455,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // - 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) + DeferredAnchor::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)); @@ -6424,7 +6465,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call_u32 = call_node_index .as_u32() .expect("call node should not be NodeIndex::NONE"); - DynamicClassAnchor::ScopeOffset { + DeferredAnchor::ScopeOffset { scope, offset: call_u32 - anchor_u32, } @@ -6507,11 +6548,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) } + /// Infer field types for functional `NamedTuple` in deferred phase. + /// + /// This is called during `infer_deferred_types` to infer field types after the `NamedTuple` + /// definition is complete. This enables support for recursive namedtuples and proper + /// handling of string annotations like `("x", "int")`. + fn infer_functional_namedtuple_deferred(&mut self, arguments: &ast::Arguments) { + // The second positional argument is the fields list/tuple (if present). + let Some(fields_arg) = arguments.args.get(1) else { + return; + }; + + // Get the elements from the list or tuple literal. + let elements: &[ast::Expr] = match fields_arg { + ast::Expr::List(list) => &list.elts, + ast::Expr::Tuple(tuple) => &tuple.elts, + _ => return, + }; + + // Infer each field's type expression in deferred mode. + for elt in elements { + // Each element should be a tuple like ("field_name", type). + let field_spec_elts: &[ast::Expr] = match elt { + ast::Expr::Tuple(tuple) => &tuple.elts, + ast::Expr::List(list) => &list.elts, + _ => continue, + }; + if field_spec_elts.len() == 2 { + // Infer the type expression (second element) as an annotation. + let field_type_expr = &field_spec_elts[1]; + self.infer_annotation_expression( + field_type_expr, + DeferredExpressionState::Deferred, + ); + } + } + } + /// Infer a `typing.NamedTuple(typename, fields)` or `collections.namedtuple(typename, field_names)` call. /// /// This method *does not* call `infer_expression` on the object being called; /// it is assumed that the type for this AST node has already been inferred before this method is called. - #[expect(clippy::type_complexity)] fn infer_namedtuple_call_expression( &mut self, call_expr: &ast::ExprCall, @@ -6742,27 +6819,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; // Handle fields based on which namedtuple variant. - let (fields, has_known_fields): (Box<[(Name, Type<'db>, Option>)]>, bool) = + // + // For `typing.NamedTuple`, we only extract field names here; types are computed lazily + // via deferred inference to support recursive namedtuples and string annotations. + // + // For `collections.namedtuple`, all types are `Any`, so we just need names and default + // types. + let (raw_fields, field_defaults, has_known_fields): (Box<[Name]>, Box<[Type<'db>]>, bool) = match kind { NamedTupleKind::Typing => { - let fields = self - .extract_typing_namedtuple_fields(fields_arg, fields_type) - .or_else(|| self.extract_typing_namedtuple_fields_from_ast(fields_arg)); + // Extract field names only (types will be computed lazily). + let field_names = self + .extract_typing_namedtuple_field_names(fields_arg, fields_type) + .or_else(|| { + self.extract_typing_namedtuple_field_names_from_ast(fields_arg) + }); // Validate field names if we have known fields. - if let Some(ref fields) = fields { - let field_names: Vec<_> = - fields.iter().map(|(name, _, _)| name.clone()).collect(); - self.report_invalid_namedtuple_field_names( - &field_names, - fields_arg, - NamedTupleKind::Typing, - ); + if let Some(ref names) = field_names { + self.report_invalid_namedtuple_field_names(names, fields_arg, kind); } // Emit diagnostic if the type is outright invalid (not an iterable) or // if we have a list/tuple literal with invalid field specs. - if fields.is_none() { + if field_names.is_none() { let iterable_any = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); if !fields_type.is_assignable_to(db, iterable_any) { @@ -6789,10 +6869,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for elt in elements { let is_valid_field_spec = matches!( elt, - ast::Expr::Tuple(t) if t.elts.len() == 2 + ast::Expr::Tuple(tuple) if tuple.elts.len() == 2 ) || matches!( elt, - ast::Expr::List(l) if l.elts.len() == 2 + ast::Expr::List(list) if list.elts.len() == 2 ); if !is_valid_field_spec { let elt_type = self.expression_type(elt); @@ -6814,8 +6894,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - let has_known_fields = fields.is_some(); - (fields.unwrap_or_default(), has_known_fields) + let has_known_fields = field_names.is_some(); + // The `typing.NamedTuple` functional form doesn't support defaults. + ( + field_names.unwrap_or_default(), + Box::new([]), + has_known_fields, + ) } NamedTupleKind::Collections => { // `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or @@ -6881,7 +6966,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.report_invalid_namedtuple_field_names( &field_names, fields_arg, - NamedTupleKind::Collections, + kind, ); } else { // Apply rename logic. @@ -6900,9 +6985,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let num_fields = field_names.len(); - let defaults_count = default_types.len(); - if defaults_count > num_fields + if default_types.len() > num_fields && let Some(defaults_kw) = defaults_kw && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, defaults_kw) @@ -6911,32 +6995,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Too many defaults for `namedtuple()`" )); diagnostic.set_primary_message(format_args!( - "Got {defaults_count} default values but only {num_fields} field names" + "Got {} default values but only {num_fields} field names", + default_types.len() )); diagnostic.info("This will raise `TypeError` at runtime"); } - let defaults_count = defaults_count.min(num_fields); - let fields = field_names - .iter() - .enumerate() - .map(|(i, field_name)| { - let default = - if defaults_count > 0 && i >= num_fields - defaults_count { - // Index into default_types: first default corresponds to first - // field that has a default. - let default_idx = i - (num_fields - defaults_count); - Some(default_types[default_idx]) - } else { - None - }; - (field_name.clone(), Type::any(), default) - }) - .collect(); - (fields, true) + // Truncate defaults to at most `num_fields`. + let default_types: Box<[Type<'db>]> = + default_types.into_iter().take(num_fields).collect(); + + (field_names, default_types, true) } else { - // Couldn't determine fields statically; attribute lookups will return Any. - (Box::new([]), false) + // Couldn't determine fields statically; attribute lookups will return `Any`. + (Box::new([]), Box::new([]), false) } } }; @@ -6947,7 +7019,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // - For assigned namedtuple calls, the Definition uniquely identifies the namedtuple. // - For dangling calls, compute a relative offset from the scope's node index. let anchor = if let Some(def) = definition { - DynamicClassAnchor::Definition(def) + DeferredAnchor::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)); @@ -6957,32 +7029,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call_u32 = call_node_index .as_u32() .expect("call node should not be NodeIndex::NONE"); - DynamicClassAnchor::ScopeOffset { + DeferredAnchor::ScopeOffset { scope, offset: call_u32 - anchor_u32, } }; - let namedtuple = DynamicNamedTupleLiteral::new(db, name, fields, has_known_fields, anchor); + let namedtuple = DynamicNamedTupleLiteral::new( + db, + name, + raw_fields, + kind, + field_defaults, + has_known_fields, + anchor, + ); Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) } - /// Extract fields from a typing.NamedTuple fields argument. - #[expect(clippy::type_complexity)] - fn extract_typing_namedtuple_fields( - &mut self, + /// Extract field names from a `typing.NamedTuple` fields argument. + /// + /// Only extracts field names; types are computed lazily via deferred inference + /// to support recursive namedtuples and string annotations. + fn extract_typing_namedtuple_field_names( + &self, fields_arg: &ast::Expr, fields_type: Type<'db>, - ) -> Option, Option>)]>> { + ) -> Option> { let db = self.db(); - let scope_id = self.scope(); - let typevar_binding_context = self.typevar_binding_context; // Try to extract from a fixed-length tuple type. let tuple_spec = fields_type.tuple_instance_spec(db)?; let fixed_tuple = tuple_spec.as_fixed_length()?; - let fields: Option> = fixed_tuple + + // Check if we have AST literals for better error locations. + let has_ast_literals = matches!(fields_arg, ast::Expr::List(_) | ast::Expr::Tuple(_)); + + // Extract field names from the inferred type. + let field_names: Option> = fixed_tuple .all_elements() .iter() .map(|field_tuple| { @@ -6992,28 +7077,70 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let [Type::StringLiteral(name), field_type] = field_fixed.all_elements() else { return None; }; - // Convert value types to type expression types (e.g., class literals to instances). - let resolved_ty = - match field_type.in_type_expression(db, scope_id, typevar_binding_context) { - Ok(ty) => ty, - Err(error) => { - // Report diagnostic for invalid type expression. - if let Some(builder) = - self.context.report_lint(&INVALID_TYPE_FORM, fields_arg) + + // Validate the field type only when fields come from a variable (no AST literals). + // When we have AST literals, we validate below with better error locations. + if !has_ast_literals + && field_type + .in_type_expression(db, self.scope(), self.typevar_binding_context) + .is_err() + { + // Report diagnostic at the fields argument (best we can do for variable case). + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, fields_arg) + { + builder.into_diagnostic(format_args!( + "Object of type `{}` is not valid as a `NamedTuple` field type", + field_type.display(db) + )); + } + } + + Some(Name::new(name.value(db))) + }) + .collect(); + + // Validate AST elements when we have direct literals (for better error locations). + if field_names.is_some() && has_ast_literals { + let elements: &[ast::Expr] = match fields_arg { + ast::Expr::List(list) => &list.elts, + ast::Expr::Tuple(tuple) => &tuple.elts, + _ => &[], + }; + for elt in elements { + // Check that each element is a valid (name, type) pair. + let field_spec_elts: &[ast::Expr] = match elt { + ast::Expr::Tuple(tuple) => &tuple.elts, + ast::Expr::List(list) => &list.elts, + _ => continue, + }; + if field_spec_elts.len() == 2 { + // Validate the type expression (will be re-inferred in deferred phase). + let field_type_expr = &field_spec_elts[1]; + + // Skip validation for string literals; they're valid string annotations + // that will be parsed as types in the deferred phase. + if !matches!(field_type_expr, ast::Expr::StringLiteral(_)) { + let field_value_ty = self.expression_type(field_type_expr); + if field_value_ty + .in_type_expression(db, self.scope(), self.typevar_binding_context) + .is_err() + { + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_FORM, field_type_expr) { builder.into_diagnostic(format_args!( "Object of type `{}` is not valid as a `NamedTuple` field type", - field_type.display(db) + field_value_ty.display(db) )); } - error.fallback_type } - }; - Some((Name::new(name.value(self.db())), resolved_ty, None)) - }) - .collect(); + } + } + } + } - fields + field_names } /// Report diagnostics for invalid field names in a namedtuple definition. @@ -7063,16 +7190,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Extract fields from a typing.NamedTuple fields argument by looking at the AST directly. - /// This handles list/tuple literals that contain (name, type) pairs. - #[expect(clippy::type_complexity)] - fn extract_typing_namedtuple_fields_from_ast( - &mut self, + /// Extract field names from a `typing.NamedTuple` fields argument by looking at the AST directly. + /// + /// Only extracts field names; types are computed lazily via deferred inference + /// to support recursive namedtuples and string annotations. + fn extract_typing_namedtuple_field_names_from_ast( + &self, fields_arg: &ast::Expr, - ) -> Option, Option>)]>> { + ) -> Option> { let db = self.db(); - let scope_id = self.scope(); - let typevar_binding_context = self.typevar_binding_context; // Get the elements from the list or tuple literal. let elements: &[ast::Expr] = match fields_arg { @@ -7081,7 +7207,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => return None, }; - let fields: Option> = elements + elements .iter() .map(|elt| { // Each element should be a tuple or list like ("field_name", type) or ["field_name", type]. @@ -7090,23 +7216,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::List(list) => &list.elts, _ => return None, }; - if field_spec_elts.len() != 2 { + + let [field_name_expr, field_type_expr] = &field_spec_elts else { return None; - } + }; // First element: field name (string literal). - let field_name_expr = &field_spec_elts[0]; let field_name_ty = self.expression_type(field_name_expr); let field_name_lit = field_name_ty.as_string_literal()?; - let field_name = Name::new(field_name_lit.value(db)); - // Second element: field type (infer as type expression). - let field_type_expr = &field_spec_elts[1]; - let field_value_ty = self.expression_type(field_type_expr); - let field_ty = field_value_ty - .in_type_expression(db, scope_id, typevar_binding_context) - .unwrap_or_else(|error| { - // Report diagnostic for invalid type expression. + // Second element: validate the type expression. String literals will be parsed in + // the deferred inference phase, so skip those here. + if !matches!(field_type_expr, ast::Expr::StringLiteral(_)) { + let field_value_ty = self.expression_type(field_type_expr); + if field_value_ty + .in_type_expression(db, self.scope(), self.typevar_binding_context) + .is_err() + { if let Some(builder) = self .context .report_lint(&INVALID_TYPE_FORM, field_type_expr) @@ -7116,14 +7242,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { field_value_ty.display(db) )); } - error.fallback_type - }); + } + } - Some((field_name, field_ty, None)) + Some(Name::new(field_name_lit.value(db))) }) - .collect(); - - fields + .collect() } /// Extract field names from a collections.namedtuple fields argument by looking at the AST directly. @@ -15582,38 +15706,3 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { }) } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum NamedTupleKind { - Collections, - Typing, -} - -impl NamedTupleKind { - const fn is_collections(self) -> bool { - matches!(self, Self::Collections) - } - - const fn is_typing(self) -> bool { - matches!(self, Self::Typing) - } - - fn from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option { - match ty { - Type::SpecialForm(SpecialFormType::NamedTuple) => Some(NamedTupleKind::Typing), - Type::FunctionLiteral(function) => function - .is_known(db, KnownFunction::NamedTuple) - .then_some(NamedTupleKind::Collections), - _ => None, - } - } -} - -impl std::fmt::Display for NamedTupleKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - NamedTupleKind::Collections => "namedtuple", - NamedTupleKind::Typing => "NamedTuple", - }) - } -}