From 8dad289062fabc59ded78ed911b14eb6173f5092 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 18 Nov 2025 17:48:36 +0000 Subject: [PATCH] [ty] Add Salsa caching to `ClassLiteral::fields` (#21512) --- Cargo.lock | 1 + crates/ty/docs/rules.md | 136 +++++++++--------- crates/ty_python_semantic/Cargo.toml | 2 +- .../src/types/bound_super.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 4 +- crates/ty_python_semantic/src/types/class.rs | 21 +-- .../src/types/diagnostic.rs | 8 +- .../src/types/infer/builder.rs | 6 +- .../src/types/typed_dict.rs | 6 +- 9 files changed, 93 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9981432cac..1b2281def1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1256,6 +1256,7 @@ dependencies = [ "compact_str", "get-size-derive2", "hashbrown 0.16.0", + "indexmap", "smallvec", ] diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index ab6973d5aa..7131564c47 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -39,7 +39,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -95,7 +95,7 @@ f(int) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -126,7 +126,7 @@ a = 1 Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -158,7 +158,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -190,7 +190,7 @@ class B(A): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -217,7 +217,7 @@ class B(A, A): ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -329,7 +329,7 @@ def test(): -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -359,7 +359,7 @@ class C(A, B): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -474,7 +474,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -529,7 +529,7 @@ a: int = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -599,7 +599,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -650,7 +650,7 @@ with 1: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -679,7 +679,7 @@ a: str Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -723,7 +723,7 @@ except ZeroDivisionError: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -756,7 +756,7 @@ class C[U](Generic[T]): ... Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo! Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -864,7 +864,7 @@ class B(metaclass=f): ... Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -896,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: error · Preview (since 1.0.0) · Related issues · -View source +View source @@ -926,7 +926,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType` Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -976,7 +976,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1002,7 +1002,7 @@ def f(a: int = ''): ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1033,7 +1033,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1067,7 +1067,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1116,7 +1116,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1141,7 +1141,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1199,7 +1199,7 @@ TODO #14889 Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -1226,7 +1226,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1256,7 +1256,7 @@ TYPE_CHECKING = '' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1286,7 +1286,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1320,7 +1320,7 @@ f(10) # Error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -1354,7 +1354,7 @@ class C: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1389,7 +1389,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1414,7 +1414,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -1447,7 +1447,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1476,7 +1476,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1500,7 +1500,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1526,7 +1526,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1553,7 +1553,7 @@ f(1, x=2) # Error raised here Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -1611,7 +1611,7 @@ def test(): -> "int": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1641,7 +1641,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1670,7 +1670,7 @@ class B(A): ... # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1697,7 +1697,7 @@ f("foo") # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1725,7 +1725,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1771,7 +1771,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1798,7 +1798,7 @@ f(x=1, y=2) # Error raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1826,7 +1826,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1851,7 +1851,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1876,7 +1876,7 @@ print(x) # NameError: name 'x' is not defined Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1913,7 +1913,7 @@ b1 < b2 < b1 # exception raised here Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1941,7 +1941,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1966,7 +1966,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2007,7 +2007,7 @@ class SubProto(BaseProto, Protocol): Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -2095,7 +2095,7 @@ a = 20 / 0 # type: ignore Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2123,7 +2123,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c' Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2155,7 +2155,7 @@ A()[0] # TypeError: 'A' object is not subscriptable Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2187,7 +2187,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2214,7 +2214,7 @@ cast(int, f()) # Redundant Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2238,7 +2238,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -2296,7 +2296,7 @@ def g(): Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -2335,7 +2335,7 @@ class D(C): ... # error: [unsupported-base] Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -2398,7 +2398,7 @@ def foo(x: int | str) -> int | str: Default level: ignore · Preview (since 0.0.1-alpha.1) · Related issues · -View source +View source @@ -2422,7 +2422,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index faf5c37881..4d4ece53eb 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -32,7 +32,7 @@ camino = { workspace = true } colored = { workspace = true } compact_str = { workspace = true } drop_bomb = { workspace = true } -get-size2 = { workspace = true } +get-size2 = { workspace = true, features = ["indexmap"]} indexmap = { workspace = true } itertools = { workspace = true } ordermap = { workspace = true } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 24d6573c28..65f9295a5e 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -396,7 +396,7 @@ impl<'db> BoundSuperType<'db> { let mut key_builder = UnionBuilder::new(db); let mut value_builder = UnionBuilder::new(db); for (name, field) in td.items(db) { - key_builder = key_builder.add(Type::string_literal(db, &name)); + key_builder = key_builder.add(Type::string_literal(db, name)); value_builder = value_builder.add(field.declared_ty); } return delegate_to( diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a99ac8b1ef..b8376a22ca 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3036,8 +3036,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if let Type::TypedDict(typed_dict) = argument_type { for (argument_type, parameter_index) in typed_dict .items(self.db) - .iter() - .map(|(_, field)| field.declared_ty) + .values() + .map(|field| field.declared_ty) .zip(&self.argument_matches[argument_index].parameters) { self.check_argument_type( diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index b23de7d424..8ac9eca111 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -7,7 +7,6 @@ use super::{ SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase, function::FunctionType, infer_expression_type, infer_unpack_types, }; -use crate::FxOrderMap; use crate::module_resolver::KnownModule; use crate::place::TypeOrigin; use crate::semantic_index::definition::{Definition, DefinitionState}; @@ -128,7 +127,7 @@ fn try_metaclass_cycle_initial<'db>( } /// A category of classes with code generation capabilities (with synthesized methods). -#[derive(Clone, Copy, Debug, PartialEq, salsa::Update, get_size2::GetSize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum CodeGeneratorKind<'db> { /// Classes decorated with `@dataclass` or similar dataclass-like decorators DataclassLike(Option>), @@ -1253,7 +1252,7 @@ impl MethodDecorator { } /// Kind-specific metadata for different types of fields -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) enum FieldKind<'db> { /// `NamedTuple` field metadata NamedTuple { default_ty: Option> }, @@ -1281,7 +1280,7 @@ pub(crate) enum FieldKind<'db> { } /// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub(crate) struct Field<'db> { /// The declared type of the field pub(crate) declared_ty: Type<'db>, @@ -2329,7 +2328,8 @@ impl<'db> ClassLiteral<'db> { || kw_only.unwrap_or(has_dataclass_param(DataclassFlags::KW_ONLY)); // Use the alias name if provided, otherwise use the field name - let parameter_name = alias.map(Name::new).unwrap_or(field_name); + let parameter_name = + Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name)); let mut parameter = if is_kw_only { Parameter::keyword_only(parameter_name) @@ -2595,7 +2595,7 @@ impl<'db> ClassLiteral<'db> { (CodeGeneratorKind::TypedDict, "get") => { let overloads = self .fields(db, specialization, field_policy) - .into_iter() + .iter() .flat_map(|(name, field)| { let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str())); @@ -2824,12 +2824,13 @@ impl<'db> ClassLiteral<'db> { /// Returns a list of all annotated attributes defined in this class, or any of its superclasses. /// /// See [`ClassLiteral::own_fields`] for more details. + #[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)] pub(crate) fn fields( self, db: &'db dyn Db, specialization: Option>, - field_policy: CodeGeneratorKind, - ) -> FxOrderMap> { + field_policy: CodeGeneratorKind<'db>, + ) -> FxIndexMap> { if field_policy == CodeGeneratorKind::NamedTuple { // NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the // fields of this class only. @@ -2877,8 +2878,8 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, specialization: Option>, field_policy: CodeGeneratorKind, - ) -> FxOrderMap> { - let mut attributes = FxOrderMap::default(); + ) -> FxIndexMap> { + let mut attributes = FxIndexMap::default(); let class_body_scope = self.body_scope(db); let table = place_table(db, class_body_scope); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 42b621dcab..ed8b1be1ab 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -26,9 +26,7 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type, infer_isolated_expression, protocol_class::ProtocolClass, }; -use crate::{ - Db, DisplaySettings, FxIndexMap, FxOrderMap, Module, ModuleName, Program, declare_lint, -}; +use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint}; use itertools::Itertools; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; use ruff_db::source::source_text; @@ -3192,7 +3190,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( typed_dict_ty: Type<'db>, full_object_ty: Option>, key_ty: Type<'db>, - items: &FxOrderMap>, + items: &FxIndexMap>, ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_KEY, key_node) { @@ -3221,7 +3219,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( .message(format_args!("TypedDict `{typed_dict_name}`")) }); - let existing_keys = items.iter().map(|(name, _)| name.as_str()); + let existing_keys = items.keys(); if let Some(suggestion) = did_you_mean(existing_keys, key) { if key_node.is_expr_string_literal() { diagnostic diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 43f569b9f2..bf1b275165 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -922,7 +922,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let kw_only_sentinel_fields: Vec<_> = class .fields(self.db(), specialization, field_policy) - .into_iter() + .iter() .filter_map(|(name, field)| { field.is_kw_only_sentinel(self.db()).then_some(name) }) @@ -7954,7 +7954,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::TypedDict(typed_dict_ty), None, key_ty, - &items, + items, ); // Return `Unknown` to prevent the overload system from generating its own error return Type::unknown(); @@ -11295,7 +11295,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty, None, slice_ty, - &typed_dict.items(db), + typed_dict.items(db), ); } else { if let Some(builder) = diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e07dbe6e60..d8f8c5c54b 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -13,7 +13,7 @@ use super::diagnostic::{ }; use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor}; use crate::types::TypeContext; -use crate::{Db, FxOrderMap}; +use crate::{Db, FxIndexMap}; use ordermap::OrderSet; @@ -54,7 +54,7 @@ impl<'db> TypedDictType<'db> { self.defining_class } - pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap> { + pub(crate) fn items(self, db: &'db dyn Db) -> &'db FxIndexMap> { let (class_literal, specialization) = self.defining_class.class_literal(db); class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict) } @@ -165,7 +165,7 @@ pub(super) fn validate_typed_dict_key_assignment<'db, 'ast>( Type::TypedDict(typed_dict), full_object_ty, Type::string_literal(db, key), - &items, + items, ); }