mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[red-knot] treat annotated assignments without RHS in stubs as bindings (#16409)
This commit is contained in:
parent
5ca6cc2cc8
commit
fdf0915283
7 changed files with 80 additions and 17 deletions
|
@ -283,4 +283,9 @@ const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
|||
// related to circular references in f-string annotations (invalid syntax)
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
|
||||
// related to circular references in stub type annotations (salsa cycle panic):
|
||||
("crates/ruff_linter/resources/test/fixtures/pycodestyle/E501_4.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_0.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_12.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_14.py", false, true),
|
||||
];
|
||||
|
|
|
@ -15,3 +15,30 @@ class Bar(Foo[Bar]): ...
|
|||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Access to attributes declarated in stubs
|
||||
|
||||
Unlike regular Python modules, stub files often omit the right-hand side in declarations, including
|
||||
in class scope. However, from the perspective of the type checker, we have to treat them as bindings
|
||||
too. That is, `symbol: type` is the same as `symbol: type = ...`.
|
||||
|
||||
One implication of this is that we'll always treat symbols in class scope as safe to be accessed
|
||||
from the class object itself. We'll never infer a "pure instance attribute" from a stub.
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
class_or_instance_var: int
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
from b import C
|
||||
|
||||
# No error here, since we treat `class_or_instance_var` as bound on the class.
|
||||
reveal_type(C.class_or_instance_var) # revealed: int
|
||||
```
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Declarations in stubs
|
||||
|
||||
Unlike regular Python modules, stub files often declare module-global variables without initializing
|
||||
them. If these symbols are then used in the same stub, applying regular logic would lead to an
|
||||
undefined variable access error.
|
||||
|
||||
However, from the perspective of the type checker, we should treat something like `symbol: type` the
|
||||
same as `symbol: type = ...`. In other words, assume these are bindings too.
|
||||
|
||||
```pyi
|
||||
from typing import Literal
|
||||
|
||||
CONSTANT: Literal[42]
|
||||
|
||||
# No error here, even though the variable is not initialized.
|
||||
uses_constant: int = CONSTANT
|
||||
```
|
|
@ -353,7 +353,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||
#[allow(unsafe_code)]
|
||||
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
||||
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
|
||||
let category = kind.category();
|
||||
let category = kind.category(self.file.is_stub(self.db.upcast()));
|
||||
let is_reexported = kind.is_reexported();
|
||||
let definition = Definition::new(
|
||||
self.db,
|
||||
|
|
|
@ -515,7 +515,7 @@ impl DefinitionKind<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
DefinitionKind::Function(_)
|
||||
|
@ -543,9 +543,10 @@ impl DefinitionKind<'_> {
|
|||
DefinitionCategory::Binding
|
||||
}
|
||||
}
|
||||
// annotated assignment is always a declaration, only a binding if there is a RHS
|
||||
// Annotated assignment is always a declaration. It is also a binding if there is a RHS
|
||||
// or if we are in a stub file. Unfortunately, it is common for stubs to omit even an `...` value placeholder.
|
||||
DefinitionKind::AnnotatedAssignment(ann_assign) => {
|
||||
if ann_assign.value.is_some() {
|
||||
if in_stub || ann_assign.value.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
DefinitionCategory::Declaration
|
||||
|
|
|
@ -119,7 +119,7 @@ fn infer_definition_types_cycle_recovery<'db>(
|
|||
) -> TypeInference<'db> {
|
||||
tracing::trace!("infer_definition_types_cycle_recovery");
|
||||
let mut inference = TypeInference::empty(input.scope(db));
|
||||
let category = input.kind(db).category();
|
||||
let category = input.kind(db).category(input.file(db).is_stub(db.upcast()));
|
||||
if category.is_declaration() {
|
||||
inference
|
||||
.declarations
|
||||
|
@ -903,7 +903,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
|
||||
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'db>) {
|
||||
debug_assert!(binding.kind(self.db()).category().is_binding());
|
||||
debug_assert!(binding
|
||||
.kind(self.db())
|
||||
.category(self.context.in_stub())
|
||||
.is_binding());
|
||||
let use_def = self.index.use_def_map(binding.file_scope(self.db()));
|
||||
let declarations = use_def.declarations_at_binding(binding);
|
||||
let mut bound_ty = ty;
|
||||
|
@ -938,7 +941,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
declaration: Definition<'db>,
|
||||
ty: TypeAndQualifiers<'db>,
|
||||
) {
|
||||
debug_assert!(declaration.kind(self.db()).category().is_declaration());
|
||||
debug_assert!(declaration
|
||||
.kind(self.db())
|
||||
.category(self.context.in_stub())
|
||||
.is_declaration());
|
||||
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
|
||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
|
@ -968,8 +974,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
definition: Definition<'db>,
|
||||
declared_and_inferred_ty: &DeclaredAndInferredType<'db>,
|
||||
) {
|
||||
debug_assert!(definition.kind(self.db()).category().is_binding());
|
||||
debug_assert!(definition.kind(self.db()).category().is_declaration());
|
||||
debug_assert!(definition
|
||||
.kind(self.db())
|
||||
.category(self.context.in_stub())
|
||||
.is_binding());
|
||||
debug_assert!(definition
|
||||
.kind(self.db())
|
||||
.category(self.context.in_stub())
|
||||
.is_declaration());
|
||||
|
||||
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
|
||||
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
|
||||
|
@ -2226,7 +2238,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
self.add_declaration(assignment.into(), definition, declared_ty);
|
||||
if self.in_stub() {
|
||||
self.add_declaration_with_binding(
|
||||
assignment.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(declared_ty.inner_type()),
|
||||
);
|
||||
} else {
|
||||
self.add_declaration(assignment.into(), definition, declared_ty);
|
||||
}
|
||||
}
|
||||
|
||||
self.infer_expression(target);
|
||||
|
|
|
@ -96,13 +96,6 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
|
|||
Cow::Borrowed("Unused blanket `type: ignore` directive"),
|
||||
Severity::Warning,
|
||||
),
|
||||
(
|
||||
DiagnosticId::lint("invalid-attribute-access"),
|
||||
Some("/src/tomllib/__init__.py"),
|
||||
Some(270..296),
|
||||
Cow::Borrowed("Cannot assign to instance attribute `__module__` from the class object `Literal[TOMLDecodeError]`"),
|
||||
Severity::Error,
|
||||
),
|
||||
];
|
||||
|
||||
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue