[ty] Keep track of type qualifiers in stub declarations without right-hand side (#19756)
Some checks are pending
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

closes https://github.com/astral-sh/ty/issues/937

## Test Plan

Regression test
This commit is contained in:
David Peter 2025-08-05 12:07:05 +02:00 committed by GitHub
parent 2d2841e20d
commit 7df7be5c7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 17 deletions

View file

@ -38,6 +38,29 @@ c.d = 2
c.e = 2 c.e = 2
``` ```
## From stubs
This is a regression test for a bug where we did not properly keep track of type qualifiers when
accessed from stub files.
`module.pyi`:
```pyi
from typing import ClassVar
class C:
a: ClassVar[int]
```
`main.py`:
```py
from module import C
c = C()
c.a = 2 # error: [invalid-attribute-access]
```
## Conflicting type qualifiers ## Conflicting type qualifiers
We currently ignore conflicting qualifiers and simply union them, which is more conservative than We currently ignore conflicting qualifiers and simply union them, which is more conservative than

View file

@ -694,7 +694,7 @@ enum IntersectionOn {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
enum DeclaredAndInferredType<'db> { enum DeclaredAndInferredType<'db> {
/// We know that both the declared and inferred types are the same. /// We know that both the declared and inferred types are the same.
AreTheSame(Type<'db>), AreTheSame(TypeAndQualifiers<'db>),
/// Declared and inferred types might be different, we need to check assignability. /// Declared and inferred types might be different, we need to check assignability.
MightBeDifferent { MightBeDifferent {
declared_ty: TypeAndQualifiers<'db>, declared_ty: TypeAndQualifiers<'db>,
@ -702,6 +702,12 @@ enum DeclaredAndInferredType<'db> {
}, },
} }
impl<'db> DeclaredAndInferredType<'db> {
fn are_the_same_type(ty: Type<'db>) -> Self {
Self::AreTheSame(ty.into())
}
}
/// Builder to infer all types in a region. /// Builder to infer all types in a region.
/// ///
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling /// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
@ -2132,7 +2138,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
); );
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty { let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty), DeclaredAndInferredType::AreTheSame(type_and_qualifiers) => {
(type_and_qualifiers, type_and_qualifiers.inner_type())
}
DeclaredAndInferredType::MightBeDifferent { DeclaredAndInferredType::MightBeDifferent {
declared_ty, declared_ty,
inferred_ty, inferred_ty,
@ -2191,7 +2199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
node, node,
definition, definition,
&DeclaredAndInferredType::AreTheSame(Type::unknown()), &DeclaredAndInferredType::are_the_same_type(Type::unknown()),
); );
} }
@ -2658,7 +2666,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
function.into(), function.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(inferred_ty), &DeclaredAndInferredType::are_the_same_type(inferred_ty),
); );
} }
@ -2818,7 +2826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.as_ref() .as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr()) .is_some_and(|d| d.is_ellipsis_literal_expr())
{ {
DeclaredAndInferredType::AreTheSame(declared_ty) DeclaredAndInferredType::are_the_same_type(declared_ty)
} else { } else {
if let Some(builder) = self if let Some(builder) = self
.context .context
@ -2831,10 +2839,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared_ty.display(self.db()) declared_ty.display(self.db())
)); ));
} }
DeclaredAndInferredType::AreTheSame(declared_ty) DeclaredAndInferredType::are_the_same_type(declared_ty)
} }
} else { } else {
DeclaredAndInferredType::AreTheSame(declared_ty) DeclaredAndInferredType::are_the_same_type(declared_ty)
}; };
self.add_declaration_with_binding( self.add_declaration_with_binding(
parameter.into(), parameter.into(),
@ -2874,7 +2882,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
parameter.into(), parameter.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(ty), &DeclaredAndInferredType::are_the_same_type(ty),
); );
} else { } else {
self.add_binding( self.add_binding(
@ -2906,7 +2914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
parameter.into(), parameter.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(ty), &DeclaredAndInferredType::are_the_same_type(ty),
); );
} else { } else {
self.add_binding( self.add_binding(
@ -3004,7 +3012,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
class_node.into(), class_node.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(class_ty), &DeclaredAndInferredType::are_the_same_type(class_ty),
); );
// if there are type parameters, then the keywords and bases are within that scope // if there are type parameters, then the keywords and bases are within that scope
@ -3078,7 +3086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
type_alias.into(), type_alias.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(type_alias_ty), &DeclaredAndInferredType::are_the_same_type(type_alias_ty),
); );
} }
@ -3433,7 +3441,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
node.into(), node.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(ty), &DeclaredAndInferredType::are_the_same_type(ty),
); );
} }
@ -3453,7 +3461,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
node.into(), node.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(pep_695_todo), &DeclaredAndInferredType::are_the_same_type(pep_695_todo),
); );
} }
@ -3473,7 +3481,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
node.into(), node.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(pep_695_todo), &DeclaredAndInferredType::are_the_same_type(pep_695_todo),
); );
} }
@ -4558,7 +4566,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
target.into(), target.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(declared.inner_type()), &DeclaredAndInferredType::AreTheSame(declared),
); );
} else { } else {
self.add_declaration(target.into(), definition, declared); self.add_declaration(target.into(), definition, declared);
@ -4876,7 +4884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
alias.into(), alias.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(binding_ty), &DeclaredAndInferredType::are_the_same_type(binding_ty),
); );
} }
@ -5125,7 +5133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding( self.add_declaration_with_binding(
alias.into(), alias.into(),
definition, definition,
&DeclaredAndInferredType::AreTheSame(submodule_type), &DeclaredAndInferredType::are_the_same_type(submodule_type),
); );
return; return;
} }