[ty] Preserve declaration order when synthesizing class fields#25249
Conversation
bba1d8c to
33a711c
Compare
| field_declarations.push((first_declaration_order, symbol_id, result)); | ||
| } | ||
|
|
||
| field_declarations.sort_by_key(|(first_declaration_order, _, _)| *first_declaration_order); |
There was a problem hiding this comment.
I'm not totally sure if this would be more or less efficient, but you could do this without an additional sort by collecting field_declarations as a BTreeMap instead of a Vec:
diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs
index 2d330f93d3..01e8d69268 100644
--- a/crates/ty_python_semantic/src/types/class/static_literal.rs
+++ b/crates/ty_python_semantic/src/types/class/static_literal.rs
@@ -7,7 +7,7 @@ use ruff_db::{
use ruff_python_ast as ast;
use ruff_python_ast::{PythonVersion, name::Name};
use ruff_text_size::{Ranged, TextRange};
-use std::cell::RefCell;
+use std::{cell::RefCell, collections::BTreeMap};
use crate::{
Db, FxIndexMap, FxIndexSet, Program, TypeQualifiers,
@@ -1873,7 +1873,7 @@ impl<'db> StaticClassLiteral<'db> {
let dataclass_kw_only_default = matches!(field_policy, CodeGeneratorKind::DataclassLike(_))
.then(|| self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY));
let mut kw_only_sentinel_field_seen = false;
- let mut field_declarations = Vec::new();
+ let mut field_declarations = BTreeMap::default();
for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() {
// Here, we exclude all declarations that are not annotated assignments. We need this because
@@ -1911,13 +1911,11 @@ impl<'db> StaticClassLiteral<'db> {
};
let result = place_from_declarations(db, declarations.clone());
- field_declarations.push((first_declaration_order, symbol_id, result));
+ field_declarations.insert(first_declaration_order, (symbol_id, result));
}
- field_declarations.sort_by_key(|(first_declaration_order, _, _)| *first_declaration_order);
-
let mut attributes = FxIndexMap::default();
- for (_, symbol_id, result) in field_declarations {
+ for (symbol_id, result) in field_declarations.into_values() {
let symbol = table.symbol(symbol_id);
let first_declaration = result.first_declaration;
let attr = result.ignore_conflicting_declarations();There was a problem hiding this comment.
(I kept it as-is for now.)
| }| { | ||
| DeclarationWithConstraint { | ||
| declaration: self.all_definitions[*declaration], | ||
| declaration_order: declaration.as_u32(), |
There was a problem hiding this comment.
ScopedDefinitionId is a pub newtype wrapper around a u32 that also defines PartialOrd and Ord -- I'm not sure there's any reason to convert it to a u32 here? That just seems to lose type safety for no real benefit. I think the declaration_order field on DeclarationWithConstraint should just be a ScopedDefinitionId.
There was a problem hiding this comment.
It's because it needs to be pub -- it's actually only pub(crate):
pub(crate) use place_state::ScopedDefinitionId;But, agree and I think it's fine to expose it.
There was a problem hiding this comment.
we might want to add something to AGENTS.md about how it's generally fine to just make things pub whenever you need to in this repo. Codex seems to go out of its way to avoid doing that, which is generally good practice in a Rust library but not really necessary here where everything is an implementation detail
| ) | ||
| } | ||
|
|
||
| fn first_reachable_declaration_order( |
There was a problem hiding this comment.
| fn first_reachable_declaration_order( | |
| /// Return the first reachable declaration that matches the passed in predicate function. | |
| fn first_reachable_declaration_order( |
| let predicates = self.predicates(); | ||
| let reachability_constraints = self.reachability_constraints(); | ||
|
|
||
| self.find_map( | ||
| |DeclarationWithConstraint { | ||
| declaration, | ||
| declaration_order, | ||
| reachability_constraint, | ||
| }| { | ||
| (predicate(declaration) | ||
| && !reachability_constraints | ||
| .evaluate(db, predicates, reachability_constraint) |
There was a problem hiding this comment.
nit: the term "predicates" is quite overloaded in this function, because there's a parameter named "predicate" and also let predicates = self.predicates(), and they mean two quite different things in the same function 😄
| let predicates = self.predicates(); | |
| let reachability_constraints = self.reachability_constraints(); | |
| self.find_map( | |
| |DeclarationWithConstraint { | |
| declaration, | |
| declaration_order, | |
| reachability_constraint, | |
| }| { | |
| (predicate(declaration) | |
| && !reachability_constraints | |
| .evaluate(db, predicates, reachability_constraint) | |
| let reachability_predicates = self.predicates(); | |
| let reachability_constraints = self.reachability_constraints(); | |
| self.find_map( | |
| |DeclarationWithConstraint { | |
| declaration, | |
| declaration_order, | |
| reachability_constraint, | |
| }| { | |
| (predicate(declaration) | |
| && !reachability_constraints | |
| .evaluate(db, reachability_predicates, reachability_constraint) |
33a711c to
020bc64
Compare
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134. |
Memory usage reportMemory usage unchanged ✅ |
|
## Summary See: #25249 (comment).
…l-sh#25249) ## Summary Prior to this change, field synthesis could accidentally follow symbol-table order instead of class-body declaration order in pathological cases like: ```python from dataclasses import InitVar, dataclass from ty_extensions import Top @DataClass class C: a: Top[int] int: InitVar[int] = 0 ``` Here, `int` can be interned before its own declaration because it already appears in `Top[int]`. This PR carries a stable declaration ordinal through declaration lookup and uses it when building `own_fields()`. Closes astral-sh/ty#3493.
…l-sh#25249) ## Summary Prior to this change, field synthesis could accidentally follow symbol-table order instead of class-body declaration order in pathological cases like: ```python from dataclasses import InitVar, dataclass from ty_extensions import Top @DataClass class C: a: Top[int] int: InitVar[int] = 0 ``` Here, `int` can be interned before its own declaration because it already appears in `Top[int]`. This PR carries a stable declaration ordinal through declaration lookup and uses it when building `own_fields()`. Closes astral-sh/ty#3493.
## Summary I fixed astral-sh/ty#3493 in #25249, but missed the case of deferred evaluation for annotations... For deferred evaluation, when we then checked whether that field was a descriptor, `class_member_with_policy` attempted meta-type lookup on the cycle marker itself and panicked. E.g., on Python 3.14, where annotations are deferred by default: ```python from dataclasses import InitVar, dataclass from ty_extensions import Top @DataClass class C: a: Top[int] int: InitVar[int] = 0 C() ``` We now apply the materialized divergent fallback before class-member lookup, consistent with the other lookup and descriptor operations. (The example reports the expected missing argument for `a` instead of panicking, and is covered under Python 3.14.) Closes astral-sh/ty#3493.
Summary
Prior to this change, field synthesis could accidentally follow symbol-table order instead of class-body declaration order in pathological cases like:
Here,
intcan be interned before its own declaration because it already appears inTop[int].This PR carries a stable declaration ordinal through declaration lookup and uses it when building
own_fields().Closes astral-sh/ty#3493.