[red-knot] treat annotated assignments without RHS in stubs as bindings (#16409)

This commit is contained in:
Mike Perlov 2025-02-28 11:45:21 -05:00 committed by GitHub
parent 5ca6cc2cc8
commit fdf0915283
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 80 additions and 17 deletions

View file

@ -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),
];

View file

@ -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
```

View file

@ -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
```

View file

@ -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,

View file

@ -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

View file

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

View file

@ -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 {