Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Preserve declaration order when synthesizing class fields#25249

Merged
charliermarsh merged 2 commits into
mainfrom
charlie/declaration-order
May 20, 2026
Merged

[ty] Preserve declaration order when synthesizing class fields#25249
charliermarsh merged 2 commits into
mainfrom
charlie/declaration-order

Conversation

@charliermarsh

Copy link
Copy Markdown
Member

Summary

Prior to this change, field synthesis could accidentally follow symbol-table order instead of class-body declaration order in pathological cases like:

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.

@charliermarsh charliermarsh added bug Something isn't working ty Multi-file analysis & type inference labels May 19, 2026
@charliermarsh charliermarsh force-pushed the charlie/declaration-order branch 2 times, most recently from bba1d8c to 33a711c Compare May 20, 2026 00:03
@charliermarsh charliermarsh marked this pull request as ready for review May 20, 2026 09:08
@astral-sh-bot astral-sh-bot Bot requested a review from oconnor663 May 20, 2026 09:08

@AlexWaygood AlexWaygood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

field_declarations.push((first_declaration_order, symbol_id, result));
}

field_declarations.sort_by_key(|(first_declaration_order, _, _)| *first_declaration_order);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I kept it as-is for now.)

Comment thread crates/ty_python_core/src/use_def.rs Outdated
}| {
DeclarationWithConstraint {
declaration: self.all_definitions[*declaration],
declaration_order: declaration.as_u32(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good call.

)
}

fn first_reachable_declaration_order(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn first_reachable_declaration_order(
/// Return the first reachable declaration that matches the passed in predicate function.
fn first_reachable_declaration_order(

Comment on lines +1191 to +1202
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😄

Suggested change
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)

@charliermarsh charliermarsh force-pushed the charlie/declaration-order branch from 33a711c to 020bc64 Compare May 20, 2026 09:52
@astral-sh-bot

astral-sh-bot Bot commented May 20, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The 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.

@astral-sh-bot

astral-sh-bot Bot commented May 20, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@charliermarsh charliermarsh enabled auto-merge (squash) May 20, 2026 09:55
@astral-sh-bot

astral-sh-bot Bot commented May 20, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@charliermarsh charliermarsh merged commit f242f37 into main May 20, 2026
58 checks passed
@charliermarsh charliermarsh deleted the charlie/declaration-order branch May 20, 2026 09:57
charliermarsh added a commit that referenced this pull request May 20, 2026
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…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.
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
…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.
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
charliermarsh added a commit that referenced this pull request May 29, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

panic: Type::find_name_in_mro() should return Some() when called on a meta-type

3 participants