Repository: astral-sh/ruff
File: crates/ty_python_semantic/src/types/class.rs (Rust)

Code before:
/// 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,), {})
/// Foo2 = type("Foo", (Base,), {})
/// # Foo1 and Foo2 are distinct types
/// ```
///
/// 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()`).
    #[returns(ref)]
    pub name: Name,

    /// The base classes (from the second argument to `type()`).
    #[returns(deref)]
    pub bases: Box<[ClassBase<'db>]>,

    /// The scope containing the `type()` call.
    pub scope: ScopeId<'db>,

    /// The anchor for this dynamic class, providing stable identity.
    ///
    /// - `Definition`: The `type()` call is assigned to a variable. The definition
    ///   uniquely identifies this class and can be used to find the `type()` call.
    /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset
    ///   is relative to the enclosing scope's anchor node index.
    pub anchor: DynamicClassAnchor<'db>,

    /// The class members from the namespace dict (third argument to `type()`).
    /// Each entry is a (name, type) pair extracted from the dict literal.
    #[returns(deref)]
    pub members: Box<[(Name, Type<'db>)]>,

    /// Whether the namespace dict (third argument) is dynamic (not a literal dict,
    /// or contains non-string-literal keys). When true, attribute lookups on this
    /// class and its instances return `Unknown` instead of failing.
    pub has_dynamic_namespace: bool,

    /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
    /// or passed to `dataclass()` as a function.
    pub dataclass_params: Option<DataclassParams<'db>>,
}

/// Anchor for identifying a dynamic class literal.

Review comment (suggestion):
Do we need to store `scope` as a separate field, or could we do something like this (all tests pass):

```diff
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 5479d05080..f561ddd611 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -4731,9 +4731,6 @@ pub struct DynamicClassLiteral<'db> {
     #[returns(deref)]
     pub bases: Box<[ClassBase<'db>]>,
 
-    /// The scope containing the `type()` call.
-    pub scope: ScopeId<'db>,
-
     /// The anchor for this dynamic class, providing stable identity.
     ///
     /// - `Definition`: The `type()` call is assigned to a variable. The definition
@@ -4776,7 +4773,7 @@ pub enum DynamicClassAnchor<'db> {
     ///
     /// The offset is relative to the enclosing scope's anchor node index.
     /// For module scope, this is equivalent to an absolute index (anchor is 0).
-    ScopeOffset(u32),
+    ScopeOffset { scope: ScopeId<'db>, offset: u32 },
 }
 
 impl get_size2::GetSize for DynamicClassLiteral<'_> {}
@@ -4787,7 +4784,14 @@ impl<'db> DynamicClassLiteral<'db> {
     pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
         match self.anchor(db) {
             DynamicClassAnchor::Definition(definition) => Some(definition),
-            DynamicClassAnchor::ScopeOffset(_) => None,
+            DynamicClassAnchor::ScopeOffset { .. } => None,
+        }
+    }
+
+    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,
         }
     }
 
@@ -4814,7 +4818,7 @@ impl<'db> DynamicClassLiteral<'db> {
                     .expect("DynamicClassAnchor::Definition should only be used for assignments")
                     .range()
             }
-            DynamicClassAnchor::ScopeOffset(offset) => {
+            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
@@ -5070,7 +5074,6 @@ impl<'db> DynamicClassLiteral<'db> {
             db,
             self.name(db).clone(),
             self.bases(db),
-            self.scope(db),
             self.anchor(db),
             self.members(db),
             self.has_dynamic_namespace(db),
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 43a4c4c073..bcf219a62b 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -6193,14 +6193,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
             let call_u32 = call_node_index
                 .as_u32()
                 .expect("call node should not be NodeIndex::NONE");
-            DynamicClassAnchor::ScopeOffset(call_u32 - anchor_u32)
+            DynamicClassAnchor::ScopeOffset {
+                scope,
+                offset: call_u32 - anchor_u32,
+            }
         };
 
         let dynamic_class = DynamicClassLiteral::new(
             db,
             name,
             bases,
-            scope,
             anchor,
             members,
             has_dynamic_namespace,
```

Code after:
/// ```python
/// Foo1 = type("Foo", (Base,), {})
/// Foo2 = type("Foo", (Base,), {})
/// # Foo1 and Foo2 are distinct types
/// ```
///
/// 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()`).
    #[returns(ref)]
    pub name: Name,

    /// The base classes (from the second argument to `type()`).
    #[returns(deref)]
    pub bases: Box<[ClassBase<'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.
    #[returns(deref)]
    pub members: Box<[(Name, Type<'db>)]>,

    /// Whether the namespace dict (third argument) is dynamic (not a literal dict,
    /// or contains non-string-literal keys). When true, attribute lookups on this
    /// class and its instances return `Unknown` instead of failing.
    pub has_dynamic_namespace: bool,

    /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
    /// or passed to `dataclass()` as a function.
    pub dataclass_params: Option<DataclassParams<'db>>,
}

/// Anchor for identifying a dynamic class literal.
///
/// This enum provides stable identity for `DynamicClassLiteral`:
/// - For assigned calls, the `Definition` uniquely identifies the class.
/// - For dangling calls, a relative offset provides stable identity.
#[derive(
    Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update, get_size2::GetSize,