[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
```
## 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
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)]
enum DeclaredAndInferredType<'db> {
/// 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.
MightBeDifferent {
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.
///
/// 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 {
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
DeclaredAndInferredType::AreTheSame(type_and_qualifiers) => {
(type_and_qualifiers, type_and_qualifiers.inner_type())
}
DeclaredAndInferredType::MightBeDifferent {
declared_ty,
inferred_ty,
@ -2191,7 +2199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
node,
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(
function.into(),
definition,
&DeclaredAndInferredType::AreTheSame(inferred_ty),
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
);
}
@ -2818,7 +2826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr())
{
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
} else {
if let Some(builder) = self
.context
@ -2831,10 +2839,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared_ty.display(self.db())
));
}
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
}
} else {
DeclaredAndInferredType::AreTheSame(declared_ty)
DeclaredAndInferredType::are_the_same_type(declared_ty)
};
self.add_declaration_with_binding(
parameter.into(),
@ -2874,7 +2882,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
parameter.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
} else {
self.add_binding(
@ -2906,7 +2914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
parameter.into(),
definition,
&DeclaredAndInferredType::AreTheSame(ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
} else {
self.add_binding(
@ -3004,7 +3012,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
class_node.into(),
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
@ -3078,7 +3086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
type_alias.into(),
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(
node.into(),
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(
node.into(),
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(
node.into(),
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(
target.into(),
definition,
&DeclaredAndInferredType::AreTheSame(declared.inner_type()),
&DeclaredAndInferredType::AreTheSame(declared),
);
} else {
self.add_declaration(target.into(), definition, declared);
@ -4876,7 +4884,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.add_declaration_with_binding(
alias.into(),
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(
alias.into(),
definition,
&DeclaredAndInferredType::AreTheSame(submodule_type),
&DeclaredAndInferredType::are_the_same_type(submodule_type),
);
return;
}