mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +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)
|
// 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_15.py", true, true),
|
||||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, 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) # revealed: Literal[Bar]
|
||||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
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)]
|
#[allow(unsafe_code)]
|
||||||
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
||||||
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
|
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 is_reexported = kind.is_reexported();
|
||||||
let definition = Definition::new(
|
let definition = Definition::new(
|
||||||
self.db,
|
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 {
|
match self {
|
||||||
// functions, classes, and imports always bind, and we consider them declarations
|
// functions, classes, and imports always bind, and we consider them declarations
|
||||||
DefinitionKind::Function(_)
|
DefinitionKind::Function(_)
|
||||||
|
@ -543,9 +543,10 @@ impl DefinitionKind<'_> {
|
||||||
DefinitionCategory::Binding
|
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) => {
|
DefinitionKind::AnnotatedAssignment(ann_assign) => {
|
||||||
if ann_assign.value.is_some() {
|
if in_stub || ann_assign.value.is_some() {
|
||||||
DefinitionCategory::DeclarationAndBinding
|
DefinitionCategory::DeclarationAndBinding
|
||||||
} else {
|
} else {
|
||||||
DefinitionCategory::Declaration
|
DefinitionCategory::Declaration
|
||||||
|
|
|
@ -119,7 +119,7 @@ fn infer_definition_types_cycle_recovery<'db>(
|
||||||
) -> TypeInference<'db> {
|
) -> TypeInference<'db> {
|
||||||
tracing::trace!("infer_definition_types_cycle_recovery");
|
tracing::trace!("infer_definition_types_cycle_recovery");
|
||||||
let mut inference = TypeInference::empty(input.scope(db));
|
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() {
|
if category.is_declaration() {
|
||||||
inference
|
inference
|
||||||
.declarations
|
.declarations
|
||||||
|
@ -903,7 +903,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_binding(&mut self, node: AnyNodeRef, binding: Definition<'db>, ty: Type<'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 use_def = self.index.use_def_map(binding.file_scope(self.db()));
|
||||||
let declarations = use_def.declarations_at_binding(binding);
|
let declarations = use_def.declarations_at_binding(binding);
|
||||||
let mut bound_ty = ty;
|
let mut bound_ty = ty;
|
||||||
|
@ -938,7 +941,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
declaration: Definition<'db>,
|
declaration: Definition<'db>,
|
||||||
ty: TypeAndQualifiers<'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 use_def = self.index.use_def_map(declaration.file_scope(self.db()));
|
||||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||||
// unbound_ty is Never because for this check we don't care about unbound
|
// 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>,
|
definition: Definition<'db>,
|
||||||
declared_and_inferred_ty: &DeclaredAndInferredType<'db>,
|
declared_and_inferred_ty: &DeclaredAndInferredType<'db>,
|
||||||
) {
|
) {
|
||||||
debug_assert!(definition.kind(self.db()).category().is_binding());
|
debug_assert!(definition
|
||||||
debug_assert!(definition.kind(self.db()).category().is_declaration());
|
.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 {
|
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
|
||||||
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
|
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
|
||||||
|
@ -2226,7 +2238,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} 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);
|
self.infer_expression(target);
|
||||||
|
|
|
@ -96,13 +96,6 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[
|
||||||
Cow::Borrowed("Unused blanket `type: ignore` directive"),
|
Cow::Borrowed("Unused blanket `type: ignore` directive"),
|
||||||
Severity::Warning,
|
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 {
|
fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue